[feat] Init code
This commit is contained in:
		
						commit
						b1e9c5e62a
					
				
							
								
								
									
										11
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					yarn-error.log
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					deployment/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# vim swap file
 | 
				
			||||||
 | 
					*.swp
 | 
				
			||||||
 | 
					credential.json
 | 
				
			||||||
 | 
					coverage/
 | 
				
			||||||
 | 
					fcm_credential.json
 | 
				
			||||||
							
								
								
									
										4
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					public/
 | 
				
			||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*.d.ts
 | 
				
			||||||
							
								
								
									
										9
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "parserOptions": {
 | 
				
			||||||
 | 
					    "ecmaVersion": 2020
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "rules": {
 | 
				
			||||||
 | 
					    "no-console": [1, { "allow": ["info", "warn", "error", "table"] }],
 | 
				
			||||||
 | 
					    "no-underscore-dangle": ["error", { "allow": ["_id", "_type", "_index", "_source", "super_", "_score", "_modelName"] }]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					yarn-error.log
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# vim swap file
 | 
				
			||||||
 | 
					*.swp
 | 
				
			||||||
 | 
					credential.json
 | 
				
			||||||
 | 
					coverage/
 | 
				
			||||||
 | 
					fcm_credential.json
 | 
				
			||||||
							
								
								
									
										92
									
								
								.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					include:
 | 
				
			||||||
 | 
					  - template: Security/SAST.gitlab-ci.yml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sast:
 | 
				
			||||||
 | 
					  stage: build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					nodejs-scan-sast:
 | 
				
			||||||
 | 
					  rules:
 | 
				
			||||||
 | 
					    - if: $SAST_DISABLED
 | 
				
			||||||
 | 
					      when: never
 | 
				
			||||||
 | 
					    - if: $CI_COMMIT_BRANCH != "develop"
 | 
				
			||||||
 | 
					      when: never
 | 
				
			||||||
 | 
					    - if: $CI_COMMIT_BRANCH &&
 | 
				
			||||||
 | 
					          $SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/
 | 
				
			||||||
 | 
					      exists:
 | 
				
			||||||
 | 
					        - 'package.json'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					eslint-sast:
 | 
				
			||||||
 | 
					  rules:
 | 
				
			||||||
 | 
					    - if: $SAST_DISABLED
 | 
				
			||||||
 | 
					      when: never
 | 
				
			||||||
 | 
					    - if: $CI_COMMIT_BRANCH != "develop"
 | 
				
			||||||
 | 
					      when: never
 | 
				
			||||||
 | 
					    - if: $CI_COMMIT_BRANCH &&
 | 
				
			||||||
 | 
					          $SAST_DEFAULT_ANALYZERS =~ /eslint/
 | 
				
			||||||
 | 
					      exists:
 | 
				
			||||||
 | 
					        - '**/*.html'
 | 
				
			||||||
 | 
					        - '**/*.js'
 | 
				
			||||||
 | 
					        - '**/*.jsx'
 | 
				
			||||||
 | 
					        - '**/*.ts'
 | 
				
			||||||
 | 
					        - '**/*.tsx'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					stages:
 | 
				
			||||||
 | 
					  - build
 | 
				
			||||||
 | 
					  - deploy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dev-build-job:
 | 
				
			||||||
 | 
					  stage: build
 | 
				
			||||||
 | 
					  only:
 | 
				
			||||||
 | 
					    - develop
 | 
				
			||||||
 | 
					    - merge_requests
 | 
				
			||||||
 | 
					  image: docker:stable
 | 
				
			||||||
 | 
					  variables:
 | 
				
			||||||
 | 
					    POSTGRES_USER: postgres
 | 
				
			||||||
 | 
					    POSTGRES_PASSWORD: password
 | 
				
			||||||
 | 
					    POSTGRES_DB: demo_server
 | 
				
			||||||
 | 
					  services:
 | 
				
			||||||
 | 
					    - name: registry.lawsnote.com/postgres:13-pgroonga
 | 
				
			||||||
 | 
					      alias: postgres
 | 
				
			||||||
 | 
					    - name: redis:5-alpine
 | 
				
			||||||
 | 
					      alias: redis
 | 
				
			||||||
 | 
					  script:
 | 
				
			||||||
 | 
					    # 用 DATE-CI_COMMIT_SHA 當做 docker image 的 tag
 | 
				
			||||||
 | 
					    - DOCKER_IMAGE_TAG=$(date +%Y%m%d%H%M%S)-${CI_COMMIT_SHA:0:8}
 | 
				
			||||||
 | 
					    - DOCKER_BUILDKIT=1 docker build
 | 
				
			||||||
 | 
					      --ssh default="$SSH_PRIVATE_KEY"
 | 
				
			||||||
 | 
					      --build-arg CI_COMMIT_SHA="$CI_COMMIT_SHA"
 | 
				
			||||||
 | 
					      -t registry.lawsnote.com/professorx-dev:$DOCKER_IMAGE_TAG .
 | 
				
			||||||
 | 
					    # 使用 build 好的 image 測試
 | 
				
			||||||
 | 
					    - docker run 
 | 
				
			||||||
 | 
					      --rm
 | 
				
			||||||
 | 
					      -e DB_HOST=$POSTGRES_PORT_5432_TCP_ADDR
 | 
				
			||||||
 | 
					      -e DB_USER=postgres
 | 
				
			||||||
 | 
					      -e DB_PASSWORD=password
 | 
				
			||||||
 | 
					      -e DB_NAME=demo_server
 | 
				
			||||||
 | 
					      -e REDIS_HOST=$REDIS_PORT_6379_TCP_ADDR
 | 
				
			||||||
 | 
					      -e SMS_VENDER=empty
 | 
				
			||||||
 | 
					      -e NODE_ENV=test
 | 
				
			||||||
 | 
					      registry.lawsnote.com/professorx-dev:$DOCKER_IMAGE_TAG yarn test-with-db
 | 
				
			||||||
 | 
					    # push docker image
 | 
				
			||||||
 | 
					    - docker push registry.lawsnote.com/professorx-dev:$DOCKER_IMAGE_TAG
 | 
				
			||||||
 | 
					    # tag latest
 | 
				
			||||||
 | 
					    - docker tag registry.lawsnote.com/professorx-dev:$DOCKER_IMAGE_TAG registry.lawsnote.com/professorx-dev:latest
 | 
				
			||||||
 | 
					    - docker push registry.lawsnote.com/professorx-dev:latest
 | 
				
			||||||
 | 
					    # delete local image
 | 
				
			||||||
 | 
					    - docker rmi registry.lawsnote.com/professorx-dev:latest
 | 
				
			||||||
 | 
					    # 執行 Galactus 來刪除不必要的 image,只保留 10 個舊版
 | 
				
			||||||
 | 
					    - docker run --rm --env TARGET_IMAGE=professorx-dev --env KEEP_COUNT=10 --env FORCE=1 registry.lawsnote.com/galactus:latest
 | 
				
			||||||
 | 
					  tags:
 | 
				
			||||||
 | 
					    - docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dev-deploy-job:
 | 
				
			||||||
 | 
					  stage: deploy
 | 
				
			||||||
 | 
					  only:
 | 
				
			||||||
 | 
					    - develop
 | 
				
			||||||
 | 
					  script:
 | 
				
			||||||
 | 
					    - docker pull registry.lawsnote.com/professorx-dev:latest
 | 
				
			||||||
 | 
					    - if [ "$(docker inspect -f '{{.State.Running}}' professorx-dev 2> /dev/null)" == "true" ]; then docker rm -f -v professorx-dev; fi
 | 
				
			||||||
 | 
					    - docker run --detach --restart always --log-driver=json-file --log-opt max-size=16m --log-opt max-file=2 --publish 30041:10230 --name professorx-dev --env-file "$DEV_SERVICE_ENV" registry.lawsnote.com/professorx-dev:latest
 | 
				
			||||||
 | 
					  tags:
 | 
				
			||||||
 | 
					    - office
 | 
				
			||||||
							
								
								
									
										22
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					# syntax=docker/dockerfile:experimental
 | 
				
			||||||
 | 
					FROM node:14.14.0 as deps
 | 
				
			||||||
 | 
					USER root
 | 
				
			||||||
 | 
					WORKDIR /data
 | 
				
			||||||
 | 
					COPY package.json yarn.lock ./
 | 
				
			||||||
 | 
					#RUN apk add --no-cache git openssh-client
 | 
				
			||||||
 | 
					RUN mkdir -p /root/.ssh
 | 
				
			||||||
 | 
					RUN echo "Host git.lawsnote.com\n\tStrictHostKeyChecking no\n\tUser git\n\tHostname git.lawsnote.com\n\tPort 2222\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
 | 
				
			||||||
 | 
					# SSH key is set in docker build argument, ex: docker build --ssh default="$SSH_PRIVATE_KEY"
 | 
				
			||||||
 | 
					RUN --mount=type=ssh yarn install --frozen-lockfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FROM node:14.14.0-slim
 | 
				
			||||||
 | 
					WORKDIR /data
 | 
				
			||||||
 | 
					ARG CI_COMMIT_SHA
 | 
				
			||||||
 | 
					COPY . .
 | 
				
			||||||
 | 
					COPY --from=deps /data/node_modules ./node_modules/
 | 
				
			||||||
 | 
					#RUN apk add --no-cache ffmpeg \
 | 
				
			||||||
 | 
					#  && chown -R node:node .
 | 
				
			||||||
 | 
					RUN chown -R node:node . \
 | 
				
			||||||
 | 
					  && sed -i "s/__CI_COMMIT_SHA__/$CI_COMMIT_SHA/" ./public/html/version.html
 | 
				
			||||||
 | 
					USER node
 | 
				
			||||||
 | 
					CMD ["yarn", "start"]
 | 
				
			||||||
							
								
								
									
										53
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					# Demo Server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					KeyCloak 測試 Server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 設定環境變數
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					# 伺服器完整網址
 | 
				
			||||||
 | 
					SERVER_URL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 伺服器監聽埠
 | 
				
			||||||
 | 
					SERVER_PORT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# JWT 簽名 Secret
 | 
				
			||||||
 | 
					SERVER_JWT_SECRET
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Redis 位址
 | 
				
			||||||
 | 
					REDIS_HOST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Redis Port
 | 
				
			||||||
 | 
					REDIS_PORT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Redis 密碼,沒有則留空
 | 
				
			||||||
 | 
					REDIS_PASSWORD
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Redis 鍵值前綴
 | 
				
			||||||
 | 
					REDIS_PREFIX
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Redis 連線的資料庫號碼
 | 
				
			||||||
 | 
					REDIS_DB
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PostgreSQL 資料庫位址
 | 
				
			||||||
 | 
					DB_HOST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PostgreSQL 資料庫連接埠
 | 
				
			||||||
 | 
					DB_PORT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PostgreSQL 資料庫使用者
 | 
				
			||||||
 | 
					DB_USER
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PostgreSQL 資料庫密碼
 | 
				
			||||||
 | 
					DB_PASSWORD
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PostgreSQL 資料庫名稱
 | 
				
			||||||
 | 
					DB_NAME
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PostgreSQL 資料庫連接池最大連線數
 | 
				
			||||||
 | 
					DB_POOL_MAX
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PostgreSQL 資料庫連接池閒置連線數
 | 
				
			||||||
 | 
					DB_POOL_MIN
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										114
									
								
								bin/db-migrate.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								bin/db-migrate.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					const pg = require('pg');
 | 
				
			||||||
 | 
					const fs = require('fs');
 | 
				
			||||||
 | 
					const path = require('path');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config = require('src/config/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// schema file name format ######_name.sql
 | 
				
			||||||
 | 
					const schemaDir = path.resolve(__dirname, '..', 'schemas');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const db = new pg.Client({
 | 
				
			||||||
 | 
					  host: config.database.host,
 | 
				
			||||||
 | 
					  port: config.database.port,
 | 
				
			||||||
 | 
					  user: config.database.user,
 | 
				
			||||||
 | 
					  password: config.database.password,
 | 
				
			||||||
 | 
					  database: config.database.dbname,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					(async () => {
 | 
				
			||||||
 | 
					  await db.connect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await db.query(`select now();`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let version = -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // check migrate record table exists
 | 
				
			||||||
 | 
					  const checkTable = await db.query(
 | 
				
			||||||
 | 
					    `
 | 
				
			||||||
 | 
					    select exists(
 | 
				
			||||||
 | 
					      select 1 
 | 
				
			||||||
 | 
					      from "information_schema"."tables"
 | 
				
			||||||
 | 
					      where
 | 
				
			||||||
 | 
					        "table_schema" = $1
 | 
				
			||||||
 | 
					        and "table_name" = $2
 | 
				
			||||||
 | 
					    ) as exists
 | 
				
			||||||
 | 
					  `,
 | 
				
			||||||
 | 
					    ['public', 'migrate_log']
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (checkTable.rowCount > 0 && checkTable.rows[0].exists === true) {
 | 
				
			||||||
 | 
					    // version table exists
 | 
				
			||||||
 | 
					    const maxVersion = await db.query(`select max("version")::integer as version from "public"."migrate_log"`);
 | 
				
			||||||
 | 
					    if (maxVersion.rowCount > 0 && maxVersion.rows[0] && maxVersion.rows[0].version !== null) version = maxVersion.rows[0].version; // eslint-disable-line
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    // create version table
 | 
				
			||||||
 | 
					    await db.query(`create table "public"."migrate_log" (
 | 
				
			||||||
 | 
					      "version" integer not null primary key,
 | 
				
			||||||
 | 
					      "created_time" timestamptz not null default now()
 | 
				
			||||||
 | 
					    );`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.info(`Database Now Version: ${version}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // read all schema files
 | 
				
			||||||
 | 
					  const schemaList = await fs.promises.readdir(schemaDir);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @type {{[x: number]: boolean}}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const checkDuplicate = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @type {{version: number, filename: string}[]}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const versionList = schemaList
 | 
				
			||||||
 | 
					    .map(file => {
 | 
				
			||||||
 | 
					      const strs = file.split('_');
 | 
				
			||||||
 | 
					      const v = parseInt(strs[0], 10);
 | 
				
			||||||
 | 
					      if (isNaN(version)) throw new Error(`schema filename format error (######_name.sql)`); // eslint-disable-line
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (v in checkDuplicate) throw new Error(`schema file version (${v}) is duplicate`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      checkDuplicate[v] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return { version: v, filename: file };
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .filter(t => t && t.version > version)
 | 
				
			||||||
 | 
					    .sort((a, b) => a.version - b.version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 沒有需要更新的檔案
 | 
				
			||||||
 | 
					  if (versionList.length === 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await db.query('begin');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const vers = [];
 | 
				
			||||||
 | 
					    // write all schema file
 | 
				
			||||||
 | 
					    for (const it of versionList) {
 | 
				
			||||||
 | 
					      vers.push(`(${it.version})`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.info(`Write Version: ${it.version}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const fileContent = await fs.promises.readFile(path.resolve(schemaDir, it.filename), 'utf-8');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await db.query(fileContent);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.query(`insert into "public"."migrate_log" ("version") values ${vers.join(',')}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.query('commit');
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    await db.query('rollback');
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})()
 | 
				
			||||||
 | 
					  .then(() => {
 | 
				
			||||||
 | 
					    console.info('Database Migrate Finish');
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .catch(err => {
 | 
				
			||||||
 | 
					    console.error('Database Migrate Failed, ', err);
 | 
				
			||||||
 | 
					    process.exit(1);
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .finally(() => {
 | 
				
			||||||
 | 
					    db.end();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
							
								
								
									
										58
									
								
								bin/migrate-tool.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								bin/migrate-tool.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					const fs = require('fs');
 | 
				
			||||||
 | 
					const path = require('path');
 | 
				
			||||||
 | 
					const readline = require('readline');
 | 
				
			||||||
 | 
					const { padLeft } = require('src/utils/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const schemaDir = path.resolve(__dirname, '..', 'schemas');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					(async () => {
 | 
				
			||||||
 | 
					  const args = process.argv.slice(2);
 | 
				
			||||||
 | 
					  let filename = args[0] || '';
 | 
				
			||||||
 | 
					  if (args.length === 0) {
 | 
				
			||||||
 | 
					    // use readline
 | 
				
			||||||
 | 
					    filename = await new Promise(resolve => {
 | 
				
			||||||
 | 
					      const rl = readline.createInterface({
 | 
				
			||||||
 | 
					        input: process.stdin,
 | 
				
			||||||
 | 
					        output: process.stdout,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      rl.prompt();
 | 
				
			||||||
 | 
					      rl.question('schema filename: ', ans => {
 | 
				
			||||||
 | 
					        resolve(ans.replace(' ', '_'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        rl.close();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      rl.once('close', resolve);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (filename === '') throw new Error('no schema filename');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const schemaFiles = await fs.promises.readdir(schemaDir);
 | 
				
			||||||
 | 
					  let version = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  schemaFiles.forEach(name => {
 | 
				
			||||||
 | 
					    if (!name.endsWith('.sql')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const strInt = name.split(/_/g)[0];
 | 
				
			||||||
 | 
					    const v = parseInt(strInt, 10);
 | 
				
			||||||
 | 
					    if (isNaN(v)) return; // eslint-disable-line
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (v > version) version = v;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 版本要比最後一筆加一
 | 
				
			||||||
 | 
					  version += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const schemaName = `${padLeft(`${version}`, 6, '0')}_${filename}.sql`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const schemaText = `-- Created Time ${new Date().toISOString()}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await fs.promises.writeFile(path.resolve(schemaDir, schemaName), schemaText, 'utf-8');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.info(`File: ${path.resolve(schemaDir, schemaName)} Created!`);
 | 
				
			||||||
 | 
					})().catch(err => {
 | 
				
			||||||
 | 
					  console.error(err);
 | 
				
			||||||
 | 
					  process.exit(1);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										38
									
								
								config/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								config/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					const { env } = process;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  server: {
 | 
				
			||||||
 | 
					    url: env.SERVER_URL || 'http://localhost:10230',
 | 
				
			||||||
 | 
					    cms_api_url: env.SERVER_CMS_API_URL || 'http://localhost:10230',
 | 
				
			||||||
 | 
					    port: parseInt(env.SERVER_PORT, 10) || 10230,
 | 
				
			||||||
 | 
					    jwt_secret: env.SERVER_JWT_SECRET || 'testsecret',
 | 
				
			||||||
 | 
					    jwt_expire: parseInt(env.SERVER_JWT_EXPIRE, 10) || 60 * 60 * 24 * 30, // 30 day
 | 
				
			||||||
 | 
					    tos_url: env.SERVER_TOS_URL || 'http://localhost:10230',
 | 
				
			||||||
 | 
					    course_contract_url: env.SERVER_COURSE_CONTRACT_URL || 'http://localhost:10230',
 | 
				
			||||||
 | 
					    cms_limit_enabled: env.SERVER_CMS_LIMIT_ENABLED !== '0', // 啟用CMS routing 限制
 | 
				
			||||||
 | 
					    cms_limit_token: env.SERVER_CMS_LIMIT_TOKEN || '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  redis: {
 | 
				
			||||||
 | 
					    host: env.REDIS_HOST || 'localhost',
 | 
				
			||||||
 | 
					    port: parseInt(env.REDIS_PORT, 10) || 6379,
 | 
				
			||||||
 | 
					    password: env.REDIS_PASSWORD || '',
 | 
				
			||||||
 | 
					    prefix: env.REDIS_PREFIX || '',
 | 
				
			||||||
 | 
					    db: parseInt(env.REDIS_DB, 10) || 0,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  sso: {
 | 
				
			||||||
 | 
					    authorized_endpoint: env.SSO_AUTHORIZED_ENDPOINT || '',
 | 
				
			||||||
 | 
					    token_endpoint: env.SSO_TOKEN_ENDPOINT || '',
 | 
				
			||||||
 | 
					    logout_endpoint: env.SSO_LOGOUT_ENDPOINT || '',
 | 
				
			||||||
 | 
					    client_id: env.SSO_CLIENT_ID || '',
 | 
				
			||||||
 | 
					    client_secret: env.SSO_CLIENT_SECRET || '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  database: {
 | 
				
			||||||
 | 
					    host: env.DB_HOST || 'localhost',
 | 
				
			||||||
 | 
					    port: parseInt(env.DB_PORT, 10) || 5432,
 | 
				
			||||||
 | 
					    user: env.DB_USER || 'postgres',
 | 
				
			||||||
 | 
					    password: env.DB_PASSWORD || '',
 | 
				
			||||||
 | 
					    dbname: env.DB_NAME || 'professor_x',
 | 
				
			||||||
 | 
					    pool_max: parseInt(env.DB_POOL_MAX, 10) || 5,
 | 
				
			||||||
 | 
					    pool_min: parseInt(env.DB_POOL_MIN, 10) || 2,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										9
									
								
								constants/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								constants/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					/* eslint-disable no-bitwise */
 | 
				
			||||||
 | 
					const constants = {
 | 
				
			||||||
 | 
					  PAGE_SIZE: 20,
 | 
				
			||||||
 | 
					  OPENID_EXPIRE: 300, // 5min
 | 
				
			||||||
 | 
					  INTERNAL_REGULATION_CACHE_TTL: 1800, // 30min
 | 
				
			||||||
 | 
					  REPORT_CACHE_TTL: 600, // 10 min
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = constants;
 | 
				
			||||||
							
								
								
									
										26
									
								
								controllers/account/v1/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								controllers/account/v1/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					const { resp } = require('src/utils/response/index.js');
 | 
				
			||||||
 | 
					const redis = require('src/utils/redis.js');
 | 
				
			||||||
 | 
					const sso = require('src/utils/sso/index.js');
 | 
				
			||||||
 | 
					const { OPENID_EXPIRE } = require('src/constants/index.js');
 | 
				
			||||||
 | 
					const uuid = require('uuid');
 | 
				
			||||||
 | 
					const url = require('url');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const controller = {};
 | 
				
			||||||
 | 
					module.exports = controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					controller.loginSSO = () => async ctx => {
 | 
				
			||||||
 | 
					  const { back_url: backURL } = ctx.query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const state = uuid.v4();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authURL = sso.getAuthURL(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // store back url to cache
 | 
				
			||||||
 | 
					  const cacheKey = redis.Key.ssoLoginCache(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await redis.set(cacheKey, JSON.stringify({ back_url: backURL }), 'EX', OPENID_EXPIRE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const u = new url.URL(authURL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ctx.resp(resp.Success, { url: u.toString() });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										113
									
								
								controllers/common/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								controllers/common/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					/* eslint-disable no-bitwise */
 | 
				
			||||||
 | 
					const debug = require('debug')('ctrl:common');
 | 
				
			||||||
 | 
					const util = require('util');
 | 
				
			||||||
 | 
					const joi = require('joi');
 | 
				
			||||||
 | 
					const response = require('src/utils/response/index.js');
 | 
				
			||||||
 | 
					const { copyObject, toNumber } = require('src/utils/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { Success, InternalError, DataFormat } = response.resp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const controller = {};
 | 
				
			||||||
 | 
					module.exports = controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * api reponse function
 | 
				
			||||||
 | 
					 * @param {import('src/utils/response/index.js').respObject} resp
 | 
				
			||||||
 | 
					 * @param {string|Object} body
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function responseFunc(resp, body) {
 | 
				
			||||||
 | 
					  /** @type {import('src/utils/response/index.js').respObject} */
 | 
				
			||||||
 | 
					  const copy = copyObject(response.checkStruct(resp) ? resp : Success);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (typeof body === 'string') copy.object.message = body;
 | 
				
			||||||
 | 
					  else if (typeof body === 'object') copy.object = body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // @ts-ignore
 | 
				
			||||||
 | 
					  if (!('obj' in this)) this.obj = {};
 | 
				
			||||||
 | 
					  this.obj.status = copy.status;
 | 
				
			||||||
 | 
					  this.obj.object = copy.object;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * api error response function
 | 
				
			||||||
 | 
					 * @param {import('src/utils/response/index.js').respObject} resp
 | 
				
			||||||
 | 
					 * @param {import('src/utils/response/index.js').codeMessage=} code
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function responseError(resp, code) {
 | 
				
			||||||
 | 
					  /** @type {import('src/utils/response/index.js').respObject} */
 | 
				
			||||||
 | 
					  const copy = copyObject(response.checkStruct(resp) ? resp : InternalError);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (code && typeof code === 'object' && 'message' in code && 'code' in code) {
 | 
				
			||||||
 | 
					    copy.object = code;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const err = new response.APIError(copy.object.message, copy);
 | 
				
			||||||
 | 
					  throw err;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					controller.apiHandler = () => async (ctx, next) => {
 | 
				
			||||||
 | 
					  ctx.obj = {};
 | 
				
			||||||
 | 
					  ctx.token = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ctx.resp = responseFunc.bind(ctx);
 | 
				
			||||||
 | 
					  ctx.err = responseError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ctx.getBody = key => (ctx.request.body || {})[key];
 | 
				
			||||||
 | 
					  ctx.getFile = key => (ctx.request.files || {})[key];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // run next
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    await next();
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    debug(`Get API Throw Error: ${util.inspect(err, false, null)}`);
 | 
				
			||||||
 | 
					    // debug(err.stack);
 | 
				
			||||||
 | 
					    if (!(err instanceof response.APIError)) {
 | 
				
			||||||
 | 
					      ctx.resp(InternalError);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      ctx.obj = err.object;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (process.env.NODE_ENV !== 'production') {
 | 
				
			||||||
 | 
					      ctx.obj.object.errorStack = err.stack;
 | 
				
			||||||
 | 
					      ctx.obj.object.errorMessage = err.message;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (Object.keys(ctx.obj).length > 0) {
 | 
				
			||||||
 | 
					    ctx.status = ctx.obj.status;
 | 
				
			||||||
 | 
					    ctx.body = ctx.obj.object;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * data validate middleware
 | 
				
			||||||
 | 
					 * @param {{query?: any, header?: any, body?: any}} schema body,query and header is joi.Schema
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					controller.validate = schema => {
 | 
				
			||||||
 | 
					  if (typeof schema !== 'object') responseError(InternalError);
 | 
				
			||||||
 | 
					  const v = {};
 | 
				
			||||||
 | 
					  if ('body' in schema) v.body = joi.isSchema(schema.body) ? schema.body : joi.object(schema.body).unknown();
 | 
				
			||||||
 | 
					  if ('header' in schema) v.header = joi.isSchema(schema.header) ? schema.header : joi.object(schema.header).unknown();
 | 
				
			||||||
 | 
					  if ('query' in schema) v.query = joi.isSchema(schema.query) ? schema.query : joi.object(schema.query).unknown();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return async (ctx, next) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await joi.object(v).unknown().validateAsync({ query: ctx.query, header: ctx.headers, body: ctx.request.body });
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      debug(`data validate error: ${util.inspect(err, false, null)}`);
 | 
				
			||||||
 | 
					      responseError(DataFormat);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return next();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					controller.getAppVersion = () => async (ctx, next) => {
 | 
				
			||||||
 | 
					  // appVersion Format x.y.z (major.minor.patch)
 | 
				
			||||||
 | 
					  const appVersion = ctx.get('x-app-version');
 | 
				
			||||||
 | 
					  const appBuildNumber = toNumber(ctx.get('x-app-buildnumber'), 0);
 | 
				
			||||||
 | 
					  const appPlatform = ctx.get('x-app-platform');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Object.assign(ctx.state, { appVersion, appBuildNumber, appPlatform });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return next();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										28
									
								
								controllers/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								controllers/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					const controller = {};
 | 
				
			||||||
 | 
					module.exports = controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					controller.healthCheck = async ctx => {
 | 
				
			||||||
 | 
					  ctx.body = 'ok';
 | 
				
			||||||
 | 
					  ctx.status = 200;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					controller.appleAppSiteAssociation = async ctx => {
 | 
				
			||||||
 | 
					  ctx.status = 200;
 | 
				
			||||||
 | 
					  ctx.body = {
 | 
				
			||||||
 | 
					    applinks: {
 | 
				
			||||||
 | 
					      details: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          appID: 'CL3K9D5FDN.com.lawsnote.college.staging',
 | 
				
			||||||
 | 
					          paths: ['*'],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          appID: 'CL3K9D5FDN.com.lawsnote.college',
 | 
				
			||||||
 | 
					          paths: ['*'],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    webcredentials: {
 | 
				
			||||||
 | 
					      apps: ['CL3K9D5FDN.com.lawsnote.college.staging', 'CL3K9D5FDN.com.lawsnote.college'],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										73
									
								
								controllers/oauth/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								controllers/oauth/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					const debug = require('debug')('ctrl:common');
 | 
				
			||||||
 | 
					const util = require('util');
 | 
				
			||||||
 | 
					const url = require('url');
 | 
				
			||||||
 | 
					const sso = require('src/utils/sso/index.js');
 | 
				
			||||||
 | 
					const redis = require('src/utils/redis.js');
 | 
				
			||||||
 | 
					const { codeMessage, APIError } = require('src/utils/response/index.js');
 | 
				
			||||||
 | 
					const config = require('src/config/index.js');
 | 
				
			||||||
 | 
					const { jwt } = require('src/utils/pkgs.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const controller = {};
 | 
				
			||||||
 | 
					module.exports = controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					controller.verifyCode = () => async ctx => {
 | 
				
			||||||
 | 
					  const { code, session_state: sessionState, state } = ctx.query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // logout flow redirect tot frontend
 | 
				
			||||||
 | 
					  if (state === 'logout') {
 | 
				
			||||||
 | 
					    ctx.redirect(config.server.frontend_url);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // get back url from redis
 | 
				
			||||||
 | 
					  const cacheKey = redis.Key.ssoLoginCache(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const data = await redis.get(cacheKey);
 | 
				
			||||||
 | 
					  if (!data) ctx.throw('get login cache fail');
 | 
				
			||||||
 | 
					  const stateObj = JSON.parse(data);
 | 
				
			||||||
 | 
					  const { back_url: backURL } = stateObj;
 | 
				
			||||||
 | 
					  if (!backURL) ctx.throw('cache data missing');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const u = new url.URL(backURL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const token = await sso.getToken(code, sessionState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // generate jwt token
 | 
				
			||||||
 | 
					    const jwtToken = jwt.sign(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        user_id: `${token}-id`,
 | 
				
			||||||
 | 
					        sso: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      config.server.jwt_secret,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        expiresIn: config.server.jwt_expire,
 | 
				
			||||||
 | 
					        issuer: 'lawsnote',
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    u.searchParams.append('success', Buffer.from(JSON.stringify({ token: jwtToken })).toString('base64'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await redis.del(cacheKey);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      debug(`delete cache fail: ${util.inspect(err, false, null)}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    debug(`openid verify fail: ${util.inspect(err, false, null)}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @type {object} */
 | 
				
			||||||
 | 
					    const errObj = { ...codeMessage.CodeInternalError };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (err instanceof APIError) {
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      Object.assign(errObj, err.object.object);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    errObj.errorStack = err.stack;
 | 
				
			||||||
 | 
					    errObj.errorMessage = err.message;
 | 
				
			||||||
 | 
					    u.searchParams.append('error', Buffer.from(JSON.stringify(errObj)).toString('base64'));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ctx.redirect(u.toString());
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										3
									
								
								doc/testScript.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								doc/testScript.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					curl -X GET -H "Content-Type: application/x-www-form-urlencoded" \
 | 
				
			||||||
 | 
					    -H "Authorization: Bearer " \
 | 
				
			||||||
 | 
					    https://auth.mtfos.xyz/auth/realms/Vision/protocol/openid-connect/userinfo
 | 
				
			||||||
							
								
								
									
										10
									
								
								example.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								example.env
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					DB_PASSWORD=password
 | 
				
			||||||
 | 
					DB_NAME=professorx
 | 
				
			||||||
 | 
					DEBUG=ctrl*,knex*
 | 
				
			||||||
 | 
					DB_MIGRATE=1
 | 
				
			||||||
 | 
					SERVER_CMS_LIMIT_ENABLED=0
 | 
				
			||||||
 | 
					SSO_AUTHORIZED_ENDPOINT=https://auth.mtfos.xyz/auth/realms/Vision/protocol/openid-connect/auth
 | 
				
			||||||
 | 
					SSO_TOKEN_ENDPOINT=https://auth.mtfos.xyz/auth/realms/Vision/protocol/openid-connect/token
 | 
				
			||||||
 | 
					SSO_LOGOUT_ENDPOINT=https://auth.mtfos.xyz/auth/realms/Vision/protocol/openid-connect/logout
 | 
				
			||||||
 | 
					SSO_CLIENT_ID=vision
 | 
				
			||||||
 | 
					SSO_CLIENT_SECRET=8f6c1fa0-41a0-4218-a02d-3fb28850eb73
 | 
				
			||||||
							
								
								
									
										13
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					require('dotenv').config();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config = require('src/config/index.js');
 | 
				
			||||||
 | 
					const app = require('./server.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function runServer() {
 | 
				
			||||||
 | 
					  const server = app.listen(config.server.port, () => {
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    console.info(`server start on port ${server.address().port}`);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					runServer();
 | 
				
			||||||
							
								
								
									
										10
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "target": "es2019",
 | 
				
			||||||
 | 
					    "lib": ["es2020.string"],
 | 
				
			||||||
 | 
					    "checkJs": true,
 | 
				
			||||||
 | 
					    "module": "commonjs",
 | 
				
			||||||
 | 
					    "resolveJsonModule": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "exclude": ["node_modules"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								model/base.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								model/base.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					const db = require('src/utils/database.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Base {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.cols = [];
 | 
				
			||||||
 | 
					    this.schema = 'public';
 | 
				
			||||||
 | 
					    this.table = '';
 | 
				
			||||||
 | 
					    this.db = db;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async transaction(trxFunc) {
 | 
				
			||||||
 | 
					    if (typeof trxFunc !== 'function') throw new Error('transaction function type error');
 | 
				
			||||||
 | 
					    return this.db.transaction(trxFunc);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async checkSchema() {
 | 
				
			||||||
 | 
					    await this.db
 | 
				
			||||||
 | 
					      .withSchema(this.schema)
 | 
				
			||||||
 | 
					      .from(this.table)
 | 
				
			||||||
 | 
					      .select(...this.cols)
 | 
				
			||||||
 | 
					      .limit(1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = Base;
 | 
				
			||||||
							
								
								
									
										14
									
								
								model/common.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								model/common.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					/* eslint-disable func-names */
 | 
				
			||||||
 | 
					const Base = require('./base.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Common extends Base {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    super();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async test() {
 | 
				
			||||||
 | 
					    // nothing
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = Common;
 | 
				
			||||||
							
								
								
									
										25
									
								
								model/public/account.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								model/public/account.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					// const debug = require('debug')('models:account');
 | 
				
			||||||
 | 
					const Base = require('src/model/base.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef AccountModel
 | 
				
			||||||
 | 
					 * @property {string} id
 | 
				
			||||||
 | 
					 * @property {string} phone
 | 
				
			||||||
 | 
					 * @property {string} password with bcrypt
 | 
				
			||||||
 | 
					 * @property {string} display_name
 | 
				
			||||||
 | 
					 * @property {string} secret
 | 
				
			||||||
 | 
					 * @property {string} created_time
 | 
				
			||||||
 | 
					 * @property {string} updated_time
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Acconut extends Base {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    super();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async test() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = Acconut;
 | 
				
			||||||
							
								
								
									
										16103
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16103
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										57
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "demo-server",
 | 
				
			||||||
 | 
					  "version": "1.0.0",
 | 
				
			||||||
 | 
					  "description": "lawsnote oauth demo server",
 | 
				
			||||||
 | 
					  "main": "index.js",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "start": "node index.js",
 | 
				
			||||||
 | 
					    "migrate": "node bin/db-migrate.js",
 | 
				
			||||||
 | 
					    "test": "mocha --timeout 5000 --exit test/ && jest --passWithNoTests --runInBand --coverage .",
 | 
				
			||||||
 | 
					    "test-with-db": "npm run migrate && npm run test",
 | 
				
			||||||
 | 
					    "new-schema": "node bin/migrate-tool.js",
 | 
				
			||||||
 | 
					    "postinstall": "node -e \"var s='../',d='node_modules/src',fs=require('fs');fs.exists(d,function(e){e||fs.symlinkSync(s,d,'dir')});\""
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "keywords": [],
 | 
				
			||||||
 | 
					  "author": "Jay <admin@trj.tw>",
 | 
				
			||||||
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@google-cloud/storage": "5.4.0",
 | 
				
			||||||
 | 
					    "@koa/cors": "^3.0.0",
 | 
				
			||||||
 | 
					    "@koa/router": "^8.0.5",
 | 
				
			||||||
 | 
					    "@mtfos/swagger-generator": "git+https://github.com/otakukaze/swagger-generator.git#1.2.2",
 | 
				
			||||||
 | 
					    "axios": "0.21.0",
 | 
				
			||||||
 | 
					    "debug": "4.2.0",
 | 
				
			||||||
 | 
					    "dotenv": "^8.2.0",
 | 
				
			||||||
 | 
					    "got": "^11.8.2",
 | 
				
			||||||
 | 
					    "ioredis": "4.19.0",
 | 
				
			||||||
 | 
					    "joi": "17.3.0",
 | 
				
			||||||
 | 
					    "jsonwebtoken": "8.5.1",
 | 
				
			||||||
 | 
					    "knex": "0.21.15",
 | 
				
			||||||
 | 
					    "koa": "^2.11.0",
 | 
				
			||||||
 | 
					    "koa-body": "^4.1.1",
 | 
				
			||||||
 | 
					    "koa-logger": "^3.2.1",
 | 
				
			||||||
 | 
					    "koa-mount": "4.0.0",
 | 
				
			||||||
 | 
					    "koa-range": "0.3.0",
 | 
				
			||||||
 | 
					    "koa-static": "5.0.0",
 | 
				
			||||||
 | 
					    "pg": "8.4.1",
 | 
				
			||||||
 | 
					    "uuid": "8.3.1"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "chai": "4.2.0",
 | 
				
			||||||
 | 
					    "eslint": "^7.2.0",
 | 
				
			||||||
 | 
					    "eslint-config-airbnb-base": "^14.2.0",
 | 
				
			||||||
 | 
					    "eslint-config-prettier": "^6.11.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-import": "^2.21.2",
 | 
				
			||||||
 | 
					    "eslint-plugin-prettier": "^3.1.3",
 | 
				
			||||||
 | 
					    "jest": "^26.6.0",
 | 
				
			||||||
 | 
					    "mocha": "^8.2.0",
 | 
				
			||||||
 | 
					    "prettier": "^2.0.5",
 | 
				
			||||||
 | 
					    "supertest": "^5.0.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "nodemonConfig": {
 | 
				
			||||||
 | 
					    "ignore": [
 | 
				
			||||||
 | 
					      "credential.json",
 | 
				
			||||||
 | 
					      "fcm_credential.json"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								public/html/liveness.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								public/html/liveness.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <title></title>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  live
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										10
									
								
								public/html/readiness.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								public/html/readiness.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <title></title>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  readiness
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								public/swagger-ui/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/swagger-ui/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 665 B  | 
							
								
								
									
										
											BIN
										
									
								
								public/swagger-ui/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/swagger-ui/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 628 B  | 
							
								
								
									
										62
									
								
								public/swagger-ui/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								public/swagger-ui/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					<!-- HTML for static distribution bundle build -->
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <title>Swagger UI</title>
 | 
				
			||||||
 | 
					    <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
 | 
				
			||||||
 | 
					    <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
 | 
				
			||||||
 | 
					    <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					      html
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        overflow: -moz-scrollbars-vertical;
 | 
				
			||||||
 | 
					        overflow-y: scroll;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      *,
 | 
				
			||||||
 | 
					      *:before,
 | 
				
			||||||
 | 
					      *:after
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        box-sizing: inherit;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      body
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        margin:0;
 | 
				
			||||||
 | 
					        background: #fafafa;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div id="swagger-ui"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
 | 
				
			||||||
 | 
					    <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
 | 
				
			||||||
 | 
					    <script>
 | 
				
			||||||
 | 
					    window.onload = function() {
 | 
				
			||||||
 | 
					      // Begin Swagger UI call region
 | 
				
			||||||
 | 
					      const ui = SwaggerUIBundle({
 | 
				
			||||||
 | 
					        url: "/api-docs.json",
 | 
				
			||||||
 | 
					        dom_id: '#swagger-ui',
 | 
				
			||||||
 | 
					        deepLinking: true,
 | 
				
			||||||
 | 
					        docExpansion: 'none',
 | 
				
			||||||
 | 
					        validatorUrl: null,
 | 
				
			||||||
 | 
					        presets: [
 | 
				
			||||||
 | 
					          SwaggerUIBundle.presets.apis,
 | 
				
			||||||
 | 
					          SwaggerUIStandalonePreset
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        plugins: [
 | 
				
			||||||
 | 
					          SwaggerUIBundle.plugins.DownloadUrl
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        layout: "StandaloneLayout"
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      // End Swagger UI call region
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      window.ui = ui
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										74
									
								
								public/swagger-ui/oauth2-redirect.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								public/swagger-ui/oauth2-redirect.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html lang="en-US">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <title>Swagger UI: OAuth2 Redirect</title>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					    'use strict';
 | 
				
			||||||
 | 
					    function run () {
 | 
				
			||||||
 | 
					        var oauth2 = window.opener.swaggerUIRedirectOauth2;
 | 
				
			||||||
 | 
					        var sentState = oauth2.state;
 | 
				
			||||||
 | 
					        var redirectUrl = oauth2.redirectUrl;
 | 
				
			||||||
 | 
					        var isValid, qp, arr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (/code|token|error/.test(window.location.hash)) {
 | 
				
			||||||
 | 
					            qp = window.location.hash.substring(1);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            qp = location.search.substring(1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        arr = qp.split("&")
 | 
				
			||||||
 | 
					        arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
 | 
				
			||||||
 | 
					        qp = qp ? JSON.parse('{' + arr.join() + '}',
 | 
				
			||||||
 | 
					                function (key, value) {
 | 
				
			||||||
 | 
					                    return key === "" ? value : decodeURIComponent(value)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					        ) : {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        isValid = qp.state === sentState
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ((
 | 
				
			||||||
 | 
					          oauth2.auth.schema.get("flow") === "accessCode"||
 | 
				
			||||||
 | 
					          oauth2.auth.schema.get("flow") === "authorizationCode"
 | 
				
			||||||
 | 
					        ) && !oauth2.auth.code) {
 | 
				
			||||||
 | 
					            if (!isValid) {
 | 
				
			||||||
 | 
					                oauth2.errCb({
 | 
				
			||||||
 | 
					                    authId: oauth2.auth.name,
 | 
				
			||||||
 | 
					                    source: "auth",
 | 
				
			||||||
 | 
					                    level: "warning",
 | 
				
			||||||
 | 
					                    message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (qp.code) {
 | 
				
			||||||
 | 
					                delete oauth2.state;
 | 
				
			||||||
 | 
					                oauth2.auth.code = qp.code;
 | 
				
			||||||
 | 
					                oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                let oauthErrorMsg
 | 
				
			||||||
 | 
					                if (qp.error) {
 | 
				
			||||||
 | 
					                    oauthErrorMsg = "["+qp.error+"]: " +
 | 
				
			||||||
 | 
					                        (qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
 | 
				
			||||||
 | 
					                        (qp.error_uri ? "More info: "+qp.error_uri : "");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                oauth2.errCb({
 | 
				
			||||||
 | 
					                    authId: oauth2.auth.name,
 | 
				
			||||||
 | 
					                    source: "auth",
 | 
				
			||||||
 | 
					                    level: "error",
 | 
				
			||||||
 | 
					                    message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        window.close();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener('DOMContentLoaded', function () {
 | 
				
			||||||
 | 
					      run();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										3
									
								
								public/swagger-ui/swagger-ui-bundle.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/swagger-ui/swagger-ui-bundle.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								public/swagger-ui/swagger-ui-bundle.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/swagger-ui/swagger-ui-bundle.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								public/swagger-ui/swagger-ui-es-bundle-core.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/swagger-ui/swagger-ui-es-bundle-core.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								public/swagger-ui/swagger-ui-es-bundle-core.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/swagger-ui/swagger-ui-es-bundle-core.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								public/swagger-ui/swagger-ui-es-bundle.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/swagger-ui/swagger-ui-es-bundle.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								public/swagger-ui/swagger-ui-es-bundle.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/swagger-ui/swagger-ui-es-bundle.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								public/swagger-ui/swagger-ui-standalone-preset.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/swagger-ui/swagger-ui-standalone-preset.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								public/swagger-ui/swagger-ui-standalone-preset.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/swagger-ui/swagger-ui-standalone-preset.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										4
									
								
								public/swagger-ui/swagger-ui.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								public/swagger-ui/swagger-ui.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								public/swagger-ui/swagger-ui.css.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/swagger-ui/swagger-ui.css.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								public/swagger-ui/swagger-ui.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/swagger-ui/swagger-ui.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								public/swagger-ui/swagger-ui.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/swagger-ui/swagger-ui.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										174
									
								
								public/url.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								public/url.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,174 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "issuer": "https://auth.trj.tw/auth/realms/school",
 | 
				
			||||||
 | 
					  "authorization_endpoint": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/auth",
 | 
				
			||||||
 | 
					  "token_endpoint": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/token",
 | 
				
			||||||
 | 
					  "introspection_endpoint": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/token/introspect",
 | 
				
			||||||
 | 
					  "userinfo_endpoint": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/userinfo",
 | 
				
			||||||
 | 
					  "end_session_endpoint": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/logout",
 | 
				
			||||||
 | 
					  "jwks_uri": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/certs",
 | 
				
			||||||
 | 
					  "check_session_iframe": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/login-status-iframe.html",
 | 
				
			||||||
 | 
					  "grant_types_supported": [
 | 
				
			||||||
 | 
					    "authorization_code",
 | 
				
			||||||
 | 
					    "implicit",
 | 
				
			||||||
 | 
					    "refresh_token",
 | 
				
			||||||
 | 
					    "password",
 | 
				
			||||||
 | 
					    "client_credentials"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "response_types_supported": [
 | 
				
			||||||
 | 
					    "code",
 | 
				
			||||||
 | 
					    "none",
 | 
				
			||||||
 | 
					    "id_token",
 | 
				
			||||||
 | 
					    "token",
 | 
				
			||||||
 | 
					    "id_token token",
 | 
				
			||||||
 | 
					    "code id_token",
 | 
				
			||||||
 | 
					    "code token",
 | 
				
			||||||
 | 
					    "code id_token token"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "subject_types_supported": [
 | 
				
			||||||
 | 
					    "public",
 | 
				
			||||||
 | 
					    "pairwise"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "id_token_signing_alg_values_supported": [
 | 
				
			||||||
 | 
					    "PS384",
 | 
				
			||||||
 | 
					    "ES384",
 | 
				
			||||||
 | 
					    "RS384",
 | 
				
			||||||
 | 
					    "HS256",
 | 
				
			||||||
 | 
					    "HS512",
 | 
				
			||||||
 | 
					    "ES256",
 | 
				
			||||||
 | 
					    "RS256",
 | 
				
			||||||
 | 
					    "HS384",
 | 
				
			||||||
 | 
					    "ES512",
 | 
				
			||||||
 | 
					    "PS256",
 | 
				
			||||||
 | 
					    "PS512",
 | 
				
			||||||
 | 
					    "RS512"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "id_token_encryption_alg_values_supported": [
 | 
				
			||||||
 | 
					    "RSA-OAEP",
 | 
				
			||||||
 | 
					    "RSA-OAEP-256",
 | 
				
			||||||
 | 
					    "RSA1_5"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "id_token_encryption_enc_values_supported": [
 | 
				
			||||||
 | 
					    "A256GCM",
 | 
				
			||||||
 | 
					    "A192GCM",
 | 
				
			||||||
 | 
					    "A128GCM",
 | 
				
			||||||
 | 
					    "A128CBC-HS256",
 | 
				
			||||||
 | 
					    "A192CBC-HS384",
 | 
				
			||||||
 | 
					    "A256CBC-HS512"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "userinfo_signing_alg_values_supported": [
 | 
				
			||||||
 | 
					    "PS384",
 | 
				
			||||||
 | 
					    "ES384",
 | 
				
			||||||
 | 
					    "RS384",
 | 
				
			||||||
 | 
					    "HS256",
 | 
				
			||||||
 | 
					    "HS512",
 | 
				
			||||||
 | 
					    "ES256",
 | 
				
			||||||
 | 
					    "RS256",
 | 
				
			||||||
 | 
					    "HS384",
 | 
				
			||||||
 | 
					    "ES512",
 | 
				
			||||||
 | 
					    "PS256",
 | 
				
			||||||
 | 
					    "PS512",
 | 
				
			||||||
 | 
					    "RS512",
 | 
				
			||||||
 | 
					    "none"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "request_object_signing_alg_values_supported": [
 | 
				
			||||||
 | 
					    "PS384",
 | 
				
			||||||
 | 
					    "ES384",
 | 
				
			||||||
 | 
					    "RS384",
 | 
				
			||||||
 | 
					    "HS256",
 | 
				
			||||||
 | 
					    "HS512",
 | 
				
			||||||
 | 
					    "ES256",
 | 
				
			||||||
 | 
					    "RS256",
 | 
				
			||||||
 | 
					    "HS384",
 | 
				
			||||||
 | 
					    "ES512",
 | 
				
			||||||
 | 
					    "PS256",
 | 
				
			||||||
 | 
					    "PS512",
 | 
				
			||||||
 | 
					    "RS512",
 | 
				
			||||||
 | 
					    "none"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "response_modes_supported": [
 | 
				
			||||||
 | 
					    "query",
 | 
				
			||||||
 | 
					    "fragment",
 | 
				
			||||||
 | 
					    "form_post"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "registration_endpoint": "https://auth.trj.tw/auth/realms/school/clients-registrations/openid-connect",
 | 
				
			||||||
 | 
					  "token_endpoint_auth_methods_supported": [
 | 
				
			||||||
 | 
					    "private_key_jwt",
 | 
				
			||||||
 | 
					    "client_secret_basic",
 | 
				
			||||||
 | 
					    "client_secret_post",
 | 
				
			||||||
 | 
					    "tls_client_auth",
 | 
				
			||||||
 | 
					    "client_secret_jwt"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "token_endpoint_auth_signing_alg_values_supported": [
 | 
				
			||||||
 | 
					    "PS384",
 | 
				
			||||||
 | 
					    "ES384",
 | 
				
			||||||
 | 
					    "RS384",
 | 
				
			||||||
 | 
					    "HS256",
 | 
				
			||||||
 | 
					    "HS512",
 | 
				
			||||||
 | 
					    "ES256",
 | 
				
			||||||
 | 
					    "RS256",
 | 
				
			||||||
 | 
					    "HS384",
 | 
				
			||||||
 | 
					    "ES512",
 | 
				
			||||||
 | 
					    "PS256",
 | 
				
			||||||
 | 
					    "PS512",
 | 
				
			||||||
 | 
					    "RS512"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "claims_supported": [
 | 
				
			||||||
 | 
					    "aud",
 | 
				
			||||||
 | 
					    "sub",
 | 
				
			||||||
 | 
					    "iss",
 | 
				
			||||||
 | 
					    "auth_time",
 | 
				
			||||||
 | 
					    "name",
 | 
				
			||||||
 | 
					    "given_name",
 | 
				
			||||||
 | 
					    "family_name",
 | 
				
			||||||
 | 
					    "preferred_username",
 | 
				
			||||||
 | 
					    "email",
 | 
				
			||||||
 | 
					    "acr"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "claim_types_supported": [
 | 
				
			||||||
 | 
					    "normal"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "claims_parameter_supported": true,
 | 
				
			||||||
 | 
					  "scopes_supported": [
 | 
				
			||||||
 | 
					    "openid",
 | 
				
			||||||
 | 
					    "address",
 | 
				
			||||||
 | 
					    "email",
 | 
				
			||||||
 | 
					    "microprofile-jwt",
 | 
				
			||||||
 | 
					    "offline_access",
 | 
				
			||||||
 | 
					    "phone",
 | 
				
			||||||
 | 
					    "profile",
 | 
				
			||||||
 | 
					    "roles",
 | 
				
			||||||
 | 
					    "web-origins"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "request_parameter_supported": true,
 | 
				
			||||||
 | 
					  "request_uri_parameter_supported": true,
 | 
				
			||||||
 | 
					  "require_request_uri_registration": true,
 | 
				
			||||||
 | 
					  "code_challenge_methods_supported": [
 | 
				
			||||||
 | 
					    "plain",
 | 
				
			||||||
 | 
					    "S256"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "tls_client_certificate_bound_access_tokens": true,
 | 
				
			||||||
 | 
					  "revocation_endpoint": "https://auth.trj.tw/auth/realms/school/protocol/openid-connect/revoke",
 | 
				
			||||||
 | 
					  "revocation_endpoint_auth_methods_supported": [
 | 
				
			||||||
 | 
					    "private_key_jwt",
 | 
				
			||||||
 | 
					    "client_secret_basic",
 | 
				
			||||||
 | 
					    "client_secret_post",
 | 
				
			||||||
 | 
					    "tls_client_auth",
 | 
				
			||||||
 | 
					    "client_secret_jwt"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "revocation_endpoint_auth_signing_alg_values_supported": [
 | 
				
			||||||
 | 
					    "PS384",
 | 
				
			||||||
 | 
					    "ES384",
 | 
				
			||||||
 | 
					    "RS384",
 | 
				
			||||||
 | 
					    "HS256",
 | 
				
			||||||
 | 
					    "HS512",
 | 
				
			||||||
 | 
					    "ES256",
 | 
				
			||||||
 | 
					    "RS256",
 | 
				
			||||||
 | 
					    "HS384",
 | 
				
			||||||
 | 
					    "ES512",
 | 
				
			||||||
 | 
					    "PS256",
 | 
				
			||||||
 | 
					    "PS512",
 | 
				
			||||||
 | 
					    "RS512"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "backchannel_logout_supported": true,
 | 
				
			||||||
 | 
					  "backchannel_logout_session_supported": true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								routes/api/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								routes/api/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					const Router = require('@koa/router');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const commonCtrl = require('src/controllers/common/index.js');
 | 
				
			||||||
 | 
					const v1Router = require('./v1/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const r = new Router({ prefix: '/api' });
 | 
				
			||||||
 | 
					module.exports = r;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// set api handler middleware
 | 
				
			||||||
 | 
					r.use(commonCtrl.apiHandler(), commonCtrl.getAppVersion());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					r.use(v1Router.routes());
 | 
				
			||||||
							
								
								
									
										25
									
								
								routes/api/v1/account/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								routes/api/v1/account/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					const Router = require('@koa/router');
 | 
				
			||||||
 | 
					const joi = require('joi');
 | 
				
			||||||
 | 
					const commonCtrl = require('src/controllers/common/index.js');
 | 
				
			||||||
 | 
					const accCtrl = require('src/controllers/account/v1/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const r = new Router({ prefix: '/account' });
 | 
				
			||||||
 | 
					module.exports = r;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * get account info
 | 
				
			||||||
 | 
					 * @swagger
 | 
				
			||||||
 | 
					 * @route GET /api/v1/account/login/sso
 | 
				
			||||||
 | 
					 * @group account - account apis
 | 
				
			||||||
 | 
					 * @param {string} back_url.query.required - back to url
 | 
				
			||||||
 | 
					 * @returns {RespDefault.model} default -
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					r.get(
 | 
				
			||||||
 | 
					  '/login/sso',
 | 
				
			||||||
 | 
					  commonCtrl.validate({
 | 
				
			||||||
 | 
					    query: {
 | 
				
			||||||
 | 
					      back_url: joi.string().required(),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  accCtrl.loginSSO()
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										8
									
								
								routes/api/v1/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								routes/api/v1/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					const Router = require('@koa/router');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const accountRouter = require('./account/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const r = new Router({ prefix: '/v1' });
 | 
				
			||||||
 | 
					module.exports = r;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					r.use(accountRouter.routes());
 | 
				
			||||||
							
								
								
									
										14
									
								
								routes/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								routes/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					const Router = require('@koa/router');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const controller = require('src/controllers/index.js');
 | 
				
			||||||
 | 
					const apiRouter = require('./api/index.js');
 | 
				
			||||||
 | 
					const oauthRouter = require('./oauth/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const r = new Router();
 | 
				
			||||||
 | 
					module.exports = r;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					r.get('/', controller.healthCheck);
 | 
				
			||||||
 | 
					r.get(['/apple-app-site-association', '/.well-known/apple-app-site-association'], controller.appleAppSiteAssociation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					r.use(apiRouter.routes());
 | 
				
			||||||
 | 
					r.use(oauthRouter.routes());
 | 
				
			||||||
							
								
								
									
										8
									
								
								routes/oauth/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								routes/oauth/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					const Router = require('@koa/router');
 | 
				
			||||||
 | 
					const oauthCtrl = require('src/controllers/oauth/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const r = new Router({ prefix: '/oauth' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					r.get('/redirect', oauthCtrl.verifyCode());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = r;
 | 
				
			||||||
							
								
								
									
										29
									
								
								routes/types.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								routes/types.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					// @ts-nocheck
 | 
				
			||||||
 | 
					module.exports = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef RespDefault
 | 
				
			||||||
 | 
					 * @description 預設回傳格式
 | 
				
			||||||
 | 
					 * @property {string} message
 | 
				
			||||||
 | 
					 * @property {number} code MessageCode
 | 
				
			||||||
 | 
					 * @property {string} errorStack api error stack (除了prod以外的環境會有)
 | 
				
			||||||
 | 
					 * @property {string} errorMessage api error message (除了prod以外的環境會有)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef Pager
 | 
				
			||||||
 | 
					 * @description 頁數資訊
 | 
				
			||||||
 | 
					 * @property {number} page 目前頁數
 | 
				
			||||||
 | 
					 * @property {number} count 總筆數
 | 
				
			||||||
 | 
					 * @property {number} total 總頁數
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef Account
 | 
				
			||||||
 | 
					 * @description API回傳使用者資訊
 | 
				
			||||||
 | 
					 * @property {string} id 使用者ID
 | 
				
			||||||
 | 
					 * @property {string} phone 手機
 | 
				
			||||||
 | 
					 * @property {string} display_name 顯示名稱
 | 
				
			||||||
 | 
					 * @property {string} created_time 帳號建立時間
 | 
				
			||||||
 | 
					 * @property {string} updated_time 帳號更新時間
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
							
								
								
									
										50
									
								
								schemas/000000_init.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								schemas/000000_init.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					--
 | 
				
			||||||
 | 
					-- PostgreSQL database dump
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Dumped from database version 11.7
 | 
				
			||||||
 | 
					-- Dumped by pg_dump version 11.7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SET statement_timeout = 0;
 | 
				
			||||||
 | 
					SET lock_timeout = 0;
 | 
				
			||||||
 | 
					SET idle_in_transaction_session_timeout = 0;
 | 
				
			||||||
 | 
					SET client_encoding = 'UTF8';
 | 
				
			||||||
 | 
					SET standard_conforming_strings = on;
 | 
				
			||||||
 | 
					SELECT pg_catalog.set_config('search_path', '', false);
 | 
				
			||||||
 | 
					SET check_function_bodies = false;
 | 
				
			||||||
 | 
					SET xmloption = content;
 | 
				
			||||||
 | 
					SET client_min_messages = warning;
 | 
				
			||||||
 | 
					SET row_security = off;
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					-- Name: log; Type: SCHEMA; Schema: -; Owner: -
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE SCHEMA log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					-- Name: ltree; Type: EXTENSION; Schema: -; Owner: -
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE EXTENSION IF NOT EXISTS ltree WITH SCHEMA public;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					-- Name: EXTENSION ltree; Type: COMMENT; Schema: -; Owner: -
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON EXTENSION ltree IS 'data type for hierarchical tree-like structures';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: -
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';
 | 
				
			||||||
							
								
								
									
										99
									
								
								server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								server.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					const Koa = require('koa');
 | 
				
			||||||
 | 
					const path = require('path');
 | 
				
			||||||
 | 
					const url = require('url');
 | 
				
			||||||
 | 
					const swaggerGenerator = require('@mtfos/swagger-generator');
 | 
				
			||||||
 | 
					const { copyObject } = require('src/utils/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const app = new Koa();
 | 
				
			||||||
 | 
					const config = require('src/config/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @ts-ignore
 | 
				
			||||||
 | 
					app.proxy = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// const server = app.listen(config.server.port, () => {
 | 
				
			||||||
 | 
					//   console.log(`server start on port ${server.address().port}`); // eslint-disable-line
 | 
				
			||||||
 | 
					// });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// load middleware module
 | 
				
			||||||
 | 
					const koaLogger = require('koa-logger');
 | 
				
			||||||
 | 
					const koaCors = require('@koa/cors');
 | 
				
			||||||
 | 
					const koaBody = require('koa-body');
 | 
				
			||||||
 | 
					const koaMount = require('koa-mount');
 | 
				
			||||||
 | 
					const koaStatic = require('koa-static');
 | 
				
			||||||
 | 
					const rootRouter = require('src/routes/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const packageJSON = require('./package.json');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let swaggerDoc = null;
 | 
				
			||||||
 | 
					// generate swagger document
 | 
				
			||||||
 | 
					(async () => {
 | 
				
			||||||
 | 
					  const swaggerSpec = {
 | 
				
			||||||
 | 
					    swaggerDefinition: {
 | 
				
			||||||
 | 
					      info: {
 | 
				
			||||||
 | 
					        description: 'KeyCloak OAuth Demo Server',
 | 
				
			||||||
 | 
					        title: 'KeyCloak',
 | 
				
			||||||
 | 
					        version: packageJSON.version,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      host: new url.URL(config.server.url).host,
 | 
				
			||||||
 | 
					      basePath: '',
 | 
				
			||||||
 | 
					      produces: ['application/json'],
 | 
				
			||||||
 | 
					      schemes: ['http', 'https'],
 | 
				
			||||||
 | 
					      securityDefinitions: {
 | 
				
			||||||
 | 
					        JWT: {
 | 
				
			||||||
 | 
					          type: 'apiKey',
 | 
				
			||||||
 | 
					          in: 'header',
 | 
				
			||||||
 | 
					          name: 'Authorization',
 | 
				
			||||||
 | 
					          description: 'Bearer token',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    basedir: __dirname,
 | 
				
			||||||
 | 
					    files: ['./routes/**/*.js'],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  swaggerDoc = await swaggerGenerator.generateSpec(swaggerSpec);
 | 
				
			||||||
 | 
					})().catch(err => {
 | 
				
			||||||
 | 
					  console.error('[Error] Generate swagger doc failed, ', err);
 | 
				
			||||||
 | 
					  process.exit(1);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// production 不掛上swagger ui
 | 
				
			||||||
 | 
					if (process.env.NODE_ENV !== 'production') {
 | 
				
			||||||
 | 
					  app.use(koaMount('/api-docs/', koaStatic(path.resolve(__dirname, 'public', 'swagger-ui'), { index: 'index.html' })));
 | 
				
			||||||
 | 
					  rootRouter.get('/api-docs', async c => {
 | 
				
			||||||
 | 
					    if (!/\/$/.test(c.url)) c.redirect(`${c.url}/`);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// set swagger doc route
 | 
				
			||||||
 | 
					rootRouter.get('/api-docs.json', async c => {
 | 
				
			||||||
 | 
					  c.type = 'application/json';
 | 
				
			||||||
 | 
					  const spec = copyObject(swaggerDoc || {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (c.protocol === 'https') {
 | 
				
			||||||
 | 
					    spec.schemes = ['https'];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  c.body = spec;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.use(koaLogger());
 | 
				
			||||||
 | 
					app.use(
 | 
				
			||||||
 | 
					  koaCors({
 | 
				
			||||||
 | 
					    credentials: true,
 | 
				
			||||||
 | 
					    // allow all origin
 | 
				
			||||||
 | 
					    origin: ctx => ctx.get('origin'),
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					app.use(
 | 
				
			||||||
 | 
					  koaBody({
 | 
				
			||||||
 | 
					    multipart: true,
 | 
				
			||||||
 | 
					    formidable: {
 | 
				
			||||||
 | 
					      maxFileSize: 100 * 1024 * 1024, // 100 mb
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					app.use(koaMount('/', koaStatic(path.resolve(__dirname, 'public', 'html'))));
 | 
				
			||||||
 | 
					app.use(rootRouter.allowedMethods());
 | 
				
			||||||
 | 
					app.use(rootRouter.routes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = app;
 | 
				
			||||||
							
								
								
									
										19
									
								
								utils/database.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								utils/database.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					const knex = require('knex');
 | 
				
			||||||
 | 
					const config = require('src/config/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const pool = knex({
 | 
				
			||||||
 | 
					  client: 'pg',
 | 
				
			||||||
 | 
					  connection: {
 | 
				
			||||||
 | 
					    user: config.database.user,
 | 
				
			||||||
 | 
					    password: config.database.password,
 | 
				
			||||||
 | 
					    host: config.database.host,
 | 
				
			||||||
 | 
					    port: config.database.port,
 | 
				
			||||||
 | 
					    database: config.database.dbname,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  pool: {
 | 
				
			||||||
 | 
					    max: config.database.pool_max,
 | 
				
			||||||
 | 
					    min: config.database.pool_min,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = pool;
 | 
				
			||||||
							
								
								
									
										166
									
								
								utils/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								utils/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,166 @@
 | 
				
			|||||||
 | 
					/* eslint-disable no-restricted-globals,no-param-reassign,guard-for-in,no-restricted-syntax,no-continue */
 | 
				
			||||||
 | 
					const mod = {};
 | 
				
			||||||
 | 
					module.exports = mod;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * check value is number
 | 
				
			||||||
 | 
					 * @param {any} v input value
 | 
				
			||||||
 | 
					 * @return {boolean}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.isNumber = v => !(!isFinite(v) || v === true || v === false || v === null || v === '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * value to number
 | 
				
			||||||
 | 
					 * @param {object} v source value
 | 
				
			||||||
 | 
					 * @param {number=} defVal default value
 | 
				
			||||||
 | 
					 * @param {number=} min range min
 | 
				
			||||||
 | 
					 * @param {number=} max range max
 | 
				
			||||||
 | 
					 * @return {number}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.toNumber = (v, defVal, min, max) => {
 | 
				
			||||||
 | 
					  let defaultVal = defVal;
 | 
				
			||||||
 | 
					  let inVal = v;
 | 
				
			||||||
 | 
					  if (!mod.isNumber(defVal)) defVal = 0;
 | 
				
			||||||
 | 
					  if (typeof defVal === 'string') defaultVal = parseFloat(defVal);
 | 
				
			||||||
 | 
					  const minVal = !mod.isNumber(min) ? null : typeof min === 'string' ? parseFloat(min) : min; // eslint-disable-line
 | 
				
			||||||
 | 
					  const maxVal = !mod.isNumber(max) ? null : typeof max === 'string' ? parseFloat(max) : max; // eslint-disable-line
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!mod.isNumber(v)) return defaultVal;
 | 
				
			||||||
 | 
					  if (typeof v === 'string') inVal = parseFloat(v);
 | 
				
			||||||
 | 
					  if (minVal !== null && inVal < minVal) inVal = min;
 | 
				
			||||||
 | 
					  if (maxVal !== null && inVal > maxVal) inVal = max;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return inVal;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @exports
 | 
				
			||||||
 | 
					 * @typedef pageObject
 | 
				
			||||||
 | 
					 * @prop {number} total page number
 | 
				
			||||||
 | 
					 * @prop {number} page number
 | 
				
			||||||
 | 
					 * @prop {number} count all count number
 | 
				
			||||||
 | 
					 * @prop {number} limit query limit item
 | 
				
			||||||
 | 
					 * @prop {number} offset query offset number
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * calc page object
 | 
				
			||||||
 | 
					 * @param {number} argCount all count
 | 
				
			||||||
 | 
					 * @param {number} argPage page number
 | 
				
			||||||
 | 
					 * @param {number} argMaxItem per page item show
 | 
				
			||||||
 | 
					 * @return {pageObject}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.calcPage = (argCount, argPage, argMaxItem = 10) => {
 | 
				
			||||||
 | 
					  const count = mod.toNumber(argCount, 0, 0);
 | 
				
			||||||
 | 
					  let page = mod.toNumber(argPage, 1, 1);
 | 
				
			||||||
 | 
					  const maxItem = mod.toNumber(argMaxItem, 10, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let total = Math.ceil(count / maxItem);
 | 
				
			||||||
 | 
					  if (total < 1) total = 1;
 | 
				
			||||||
 | 
					  if (page > total) page = total;
 | 
				
			||||||
 | 
					  let offset = (page - 1) * maxItem;
 | 
				
			||||||
 | 
					  if (offset > count) offset = count;
 | 
				
			||||||
 | 
					  const limit = maxItem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const pageObject = { total, page, count, offset, limit };
 | 
				
			||||||
 | 
					  for (const key in pageObject) {
 | 
				
			||||||
 | 
					    pageObject[key] = mod.toNumber(pageObject[key]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return pageObject;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * deep copy object
 | 
				
			||||||
 | 
					 * @param {any} src
 | 
				
			||||||
 | 
					 * @return {any}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.copyObject = src => {
 | 
				
			||||||
 | 
					  if (typeof src !== 'object') return src;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isArray = Array.isArray(src);
 | 
				
			||||||
 | 
					  const copy = isArray ? [] : {};
 | 
				
			||||||
 | 
					  for (let it in src) {
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    if (isArray) it = parseInt(it, 10);
 | 
				
			||||||
 | 
					    if (typeof src[it] !== 'object') copy[it] = src[it];
 | 
				
			||||||
 | 
					    else copy[it] = mod.copyObject(src[it]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return copy;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef {(string|number)} SelectObjectAlias
 | 
				
			||||||
 | 
					 * @description pass string and length > 0 rename key to alias, other keep key name
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @exports
 | 
				
			||||||
 | 
					 * @typedef {{[key: string]: SelectObjectAlias }} SelectObjectParam
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * select object keys
 | 
				
			||||||
 | 
					 * @param {Object<string, any>} obj
 | 
				
			||||||
 | 
					 * @param {SelectObjectParam} param
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.selectObject = (obj, param) => {
 | 
				
			||||||
 | 
					  if (typeof obj !== 'object' || typeof param !== 'object') throw new Error('input arg wrong');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let newObj = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const key in param) {
 | 
				
			||||||
 | 
					    const strs = key.split('.');
 | 
				
			||||||
 | 
					    const alias = param[key];
 | 
				
			||||||
 | 
					    if (strs.length > 1) {
 | 
				
			||||||
 | 
					      if (!(strs[0] in obj)) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      newObj = { ...newObj, ...mod.selectObject(obj[strs[0]], { [strs.slice(1).join('.')]: alias }) };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const toAlias = param[key] && typeof param[key] === 'string';
 | 
				
			||||||
 | 
					    if (!(key in obj)) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    newObj[toAlias ? alias : key] = obj[key];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return newObj;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * pad string to target length
 | 
				
			||||||
 | 
					 * @param {any} v source input
 | 
				
			||||||
 | 
					 * @param {number} len target length
 | 
				
			||||||
 | 
					 * @param {number} direct pad direct (-1 left, 1 right)
 | 
				
			||||||
 | 
					 * @param {string} padChar default '0'
 | 
				
			||||||
 | 
					 * @return {string}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.pad = (v, len = 0, direct = -1, padChar = '0') => {
 | 
				
			||||||
 | 
					  if (v === null || v === undefined) return '';
 | 
				
			||||||
 | 
					  if (typeof v !== 'string' && !v.toString) return '';
 | 
				
			||||||
 | 
					  if (direct !== 1 && direct !== -1) return '';
 | 
				
			||||||
 | 
					  if (typeof v !== 'string') v = v.toString();
 | 
				
			||||||
 | 
					  if (typeof padChar !== 'string') padChar = '0';
 | 
				
			||||||
 | 
					  len = mod.toNumber(len, 0, 0);
 | 
				
			||||||
 | 
					  if (v.length < len) {
 | 
				
			||||||
 | 
					    if (direct < 0) v = `${padChar}${v}`;
 | 
				
			||||||
 | 
					    else v = `${v}${padChar}`;
 | 
				
			||||||
 | 
					    return mod.pad(v, len, direct, padChar);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return v;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * pad left
 | 
				
			||||||
 | 
					 * @param {any} v
 | 
				
			||||||
 | 
					 * @param {number} len
 | 
				
			||||||
 | 
					 * @param {string} padChar
 | 
				
			||||||
 | 
					 * @return {string}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.padLeft = (v, len = 0, padChar = '0') => mod.pad(v, len, -1, padChar);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * pad right
 | 
				
			||||||
 | 
					 * @param {any} v
 | 
				
			||||||
 | 
					 * @param {number} len
 | 
				
			||||||
 | 
					 * @param {string} padChar
 | 
				
			||||||
 | 
					 * @return {string}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.padRight = (v, len = 0, padChar = '0') => mod.pad(v, len, 1, padChar);
 | 
				
			||||||
							
								
								
									
										9
									
								
								utils/pkgs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								utils/pkgs.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					const path = require('path');
 | 
				
			||||||
 | 
					const jwt = require('jsonwebtoken');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const pkg = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pkg.path = path;
 | 
				
			||||||
 | 
					pkg.jwt = jwt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = pkg;
 | 
				
			||||||
							
								
								
									
										42
									
								
								utils/redis.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								utils/redis.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					const IORedis = require('ioredis');
 | 
				
			||||||
 | 
					const config = require('src/config/index.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Redis extends IORedis {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    let { prefix } = config.redis;
 | 
				
			||||||
 | 
					    const { host, port, password, db } = config.redis;
 | 
				
			||||||
 | 
					    if (prefix && !/:$/.test(prefix)) prefix += ':';
 | 
				
			||||||
 | 
					    super({
 | 
				
			||||||
 | 
					      host,
 | 
				
			||||||
 | 
					      port,
 | 
				
			||||||
 | 
					      password,
 | 
				
			||||||
 | 
					      db,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.prefix = prefix;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const self = this;
 | 
				
			||||||
 | 
					    // key pattern functions
 | 
				
			||||||
 | 
					    this.Key = {
 | 
				
			||||||
 | 
					      /**
 | 
				
			||||||
 | 
					       * SSO 登入暫存
 | 
				
			||||||
 | 
					       * @param {string} s state
 | 
				
			||||||
 | 
					       * @return {string}
 | 
				
			||||||
 | 
					       */
 | 
				
			||||||
 | 
					      ssoLoginCache: s => self.getKeyWithPrefix(`sso-login:${s}`),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * combine key and prefix
 | 
				
			||||||
 | 
					   * @param {string} s
 | 
				
			||||||
 | 
					   * @return {string}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getKeyWithPrefix(s) {
 | 
				
			||||||
 | 
					    if (typeof s !== 'string') throw new Error('input key not a string');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return `${this.prefix}${s}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = new Redis();
 | 
				
			||||||
							
								
								
									
										72
									
								
								utils/response/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								utils/response/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					/* eslint-disable no-underscore-dangle */
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @exports
 | 
				
			||||||
 | 
					 * @typedef {Object} codeMessage
 | 
				
			||||||
 | 
					 * @prop {number} code
 | 
				
			||||||
 | 
					 * @prop {string} message
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @exports
 | 
				
			||||||
 | 
					 * @typedef {Object} respObject
 | 
				
			||||||
 | 
					 * @prop {number} status
 | 
				
			||||||
 | 
					 * @prop {codeMessage} object
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * create respobject
 | 
				
			||||||
 | 
					 * @param {number} status
 | 
				
			||||||
 | 
					 * @param {codeMessage} codeMsg
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mod = {};
 | 
				
			||||||
 | 
					module.exports = mod;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod.respDefault = (status = 200, codeMsg) => ({ status, object: codeMsg });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod.APIError = class extends Error {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @param {string} message
 | 
				
			||||||
 | 
					   * @param {respObject} resp
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  constructor(message = '', resp) {
 | 
				
			||||||
 | 
					    super(message);
 | 
				
			||||||
 | 
					    this._object = resp || {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get object() {
 | 
				
			||||||
 | 
					    return this._object;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * check response object struct
 | 
				
			||||||
 | 
					 * @param {respObject} v
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.checkStruct = v => {
 | 
				
			||||||
 | 
					  if (typeof v !== 'object' || v === null || v === undefined) return false;
 | 
				
			||||||
 | 
					  if (!('status' in v) || !('object' in v)) return false;
 | 
				
			||||||
 | 
					  if (typeof v.object !== 'object' || !('code' in v.object) || !('message' in v.object)) return false;
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod.codeMessage = {
 | 
				
			||||||
 | 
					  CodeSuccess: { code: 1000, message: 'success' },
 | 
				
			||||||
 | 
					  CodeCreated: { code: 1001, message: 'created' },
 | 
				
			||||||
 | 
					  CodeAccepted: { code: 1002, message: 'accepted' },
 | 
				
			||||||
 | 
					  CodeDataFormat: { code: 1003, message: 'data format error' },
 | 
				
			||||||
 | 
					  CodeUnauthorized: { code: 1004, message: 'unauthorized' },
 | 
				
			||||||
 | 
					  CodeForbidden: { code: 1005, message: 'forbidden' },
 | 
				
			||||||
 | 
					  CodeNotFound: { code: 1006, message: 'not found' },
 | 
				
			||||||
 | 
					  CodeInternalError: { code: 1007, message: 'internal error' },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod.resp = {
 | 
				
			||||||
 | 
					  Success: mod.respDefault(200, mod.codeMessage.CodeSuccess),
 | 
				
			||||||
 | 
					  Created: mod.respDefault(201, mod.codeMessage.CodeCreated),
 | 
				
			||||||
 | 
					  Accepted: mod.respDefault(202, mod.codeMessage.CodeAccepted),
 | 
				
			||||||
 | 
					  DataFormat: mod.respDefault(400, mod.codeMessage.CodeDataFormat),
 | 
				
			||||||
 | 
					  Unauthorized: mod.respDefault(401, mod.codeMessage.CodeUnauthorized),
 | 
				
			||||||
 | 
					  Forbidden: mod.respDefault(403, mod.codeMessage.CodeForbidden),
 | 
				
			||||||
 | 
					  NotFound: mod.respDefault(404, mod.codeMessage.CodeNotFound),
 | 
				
			||||||
 | 
					  InternalError: mod.respDefault(500, mod.codeMessage.CodeInternalError),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										130
									
								
								utils/sso/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								utils/sso/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					const joi = require('joi');
 | 
				
			||||||
 | 
					const url = require('url');
 | 
				
			||||||
 | 
					const querystring = require('querystring');
 | 
				
			||||||
 | 
					const got = require('got');
 | 
				
			||||||
 | 
					const config = require('src/config/index.js');
 | 
				
			||||||
 | 
					const { jwt } = require('src/utils/pkgs.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mod = {};
 | 
				
			||||||
 | 
					module.exports = mod;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @return {string}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					mod.getAuthURL = state => {
 | 
				
			||||||
 | 
					  const input = joi
 | 
				
			||||||
 | 
					    .object({
 | 
				
			||||||
 | 
					      authorized_endpoint: joi.string().required(),
 | 
				
			||||||
 | 
					      token_endpoint: joi.string().required(),
 | 
				
			||||||
 | 
					      client_id: joi.string().required(),
 | 
				
			||||||
 | 
					      client_secret: joi.string().required(),
 | 
				
			||||||
 | 
					      state: joi.string().allow('', null).default(''),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .unknown()
 | 
				
			||||||
 | 
					    .validate({ ...config.sso, state });
 | 
				
			||||||
 | 
					  if (input.error) throw new Error(input.error.message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string }}}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const { value } = input;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const redirectUri = new url.URL('/oauth/redirect', config.server.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const qs = {
 | 
				
			||||||
 | 
					    client_id: value.client_id,
 | 
				
			||||||
 | 
					    scope: 'openid',
 | 
				
			||||||
 | 
					    response_type: 'code',
 | 
				
			||||||
 | 
					    redirect_uri: redirectUri.toString(),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  if (value.state) qs.state = state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return `${value.authorized_endpoint}?${querystring.stringify(qs)}`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod.getLogoutURL = () => {
 | 
				
			||||||
 | 
					  const input = joi
 | 
				
			||||||
 | 
					    .object({
 | 
				
			||||||
 | 
					      logout_endpoint: joi.string().required(),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .unknown()
 | 
				
			||||||
 | 
					    .validate({ ...config.sso });
 | 
				
			||||||
 | 
					  if (input.error) throw new Error(input.error.message);
 | 
				
			||||||
 | 
					  const redirectUri = new url.URL('/oauth/redirect', config.server.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const qs = { state: 'logout', redirect_uri: redirectUri.toString() };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return `${input.value.logout_endpoint}?${querystring.stringify(qs)}`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef SSOAccount
 | 
				
			||||||
 | 
					 * @property {string} username
 | 
				
			||||||
 | 
					 * @property {string} display_name
 | 
				
			||||||
 | 
					 * @property {string} email
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod.getToken = async (code, state) => {
 | 
				
			||||||
 | 
					  const input = joi
 | 
				
			||||||
 | 
					    .object({
 | 
				
			||||||
 | 
					      authorized_endpoint: joi.string().required(),
 | 
				
			||||||
 | 
					      token_endpoint: joi.string().required(),
 | 
				
			||||||
 | 
					      client_id: joi.string().required(),
 | 
				
			||||||
 | 
					      client_secret: joi.string().required(),
 | 
				
			||||||
 | 
					      state: joi.string().required(),
 | 
				
			||||||
 | 
					      code: joi.string().required(),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .unknown()
 | 
				
			||||||
 | 
					    .validate({ ...config.sso, state, code });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (input.error) throw new Error(input.error.message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string, code: string }}}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const { value } = input;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const redirectUri = new url.URL('/oauth/redirect', config.server.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const qs = {
 | 
				
			||||||
 | 
					    client_id: value.client_id,
 | 
				
			||||||
 | 
					    client_secret: value.client_secret,
 | 
				
			||||||
 | 
					    redirect_uri: redirectUri.toString(),
 | 
				
			||||||
 | 
					    code: value.code,
 | 
				
			||||||
 | 
					    client_session_state: value.state,
 | 
				
			||||||
 | 
					    grant_type: 'authorization_code',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const resp = await got.default.post(value.token_endpoint, {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'application/x-www-form-urlencoded',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: querystring.stringify(qs),
 | 
				
			||||||
 | 
					    responseType: 'json',
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { body } = resp;
 | 
				
			||||||
 | 
					  if (!body) throw new Error('resopnse body empty');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { id_token: idToken, access_token: accessToken } = body;
 | 
				
			||||||
 | 
					  if (!idToken) throw new Error('get id token fail');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const decoded = jwt.decode(idToken);
 | 
				
			||||||
 | 
					  if (!decoded || typeof decoded !== 'object') throw new Error('jwt decode fail');
 | 
				
			||||||
 | 
					  console.log(decoded)
 | 
				
			||||||
 | 
					  console.log(body)
 | 
				
			||||||
 | 
					  // @ts-ignore
 | 
				
			||||||
 | 
					  const { preferred_username: preferredUsername } = decoded;
 | 
				
			||||||
 | 
					  if (!preferredUsername) throw new Error('id token field missing');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const displayName = `${decoded.family_name ?? ''}${decoded.given_name ?? ''}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** @type {SSOAccount} */
 | 
				
			||||||
 | 
					  const ssoAccount = {
 | 
				
			||||||
 | 
					    username: preferredUsername.toLowerCase(),
 | 
				
			||||||
 | 
					    display_name: displayName ?? preferredUsername,
 | 
				
			||||||
 | 
					    email: decoded.email ?? '',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return ssoAccount;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user