diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8ffa241b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +/node_modules +.env.test +.env.production +.env.development +.DS_store +*.code-workspace +*.swp +/logs/*.log + +# AdminJS 관련 디렉토리 +.adminjs diff --git a/.env.example b/.env.example index 2a714eaa..7a951fa4 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,5 @@ FRONT_URL=[front url(e.g. http://localhost:3000)] AWS_ACCESS_KEY_ID=[AWS Access key ID] AWS_SECRET_ACCESS_KEY=[AWS Secret access key] AWS_S3_BUCKET_NAME=[AWS S3 Buck name] +JWT_SECRET_KEY=[JWT SERCRET KEY] +APP_URI_SCHEME=[APP_URI_SCHEME] diff --git a/.github/workflows/push_image_ecr.yml b/.github/workflows/push_image_ecr.yml index eaaf337a..17e172f7 100644 --- a/.github/workflows/push_image_ecr.yml +++ b/.github/workflows/push_image_ecr.yml @@ -1,4 +1,4 @@ -name: Push Image to Amazon ECR +name: Push Prod Image to Amazon ECR # when tagging action success on: diff --git a/.github/workflows/push_image_ecr_dev.yml b/.github/workflows/push_image_ecr_dev.yml new file mode 100644 index 00000000..ef917f5b --- /dev/null +++ b/.github/workflows/push_image_ecr_dev.yml @@ -0,0 +1,43 @@ +name: Push Dev Image to Amazon ECR +on: + pull_request: + types: + - closed + branches: + - dev + +env: + AWS_REGION: ap-northeast-2 + +jobs: + if_merged: + if: github.event.pull_request.merged == true + name: Create Release and Tag + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to AWS ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build and Push to AWS ECR + id: build_image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: taxi-back + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:dev . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev + echo "Push iamge : $ECR_REGISTRY/$ECR_REPOSITORY:dev" diff --git a/Dockerfile b/Dockerfile index 9e42ed3a..68d17b2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,9 @@ FROM node:16-alpine WORKDIR /usr/src/app COPY . . +# Install curl (for taxi-docker) +RUN apk update && apk add curl + # Install requirements RUN npm ci diff --git a/README.md b/README.md index a7014b66..de60197f 100644 --- a/README.md +++ b/README.md @@ -44,5 +44,6 @@ See [contributors](https://github.com/sparcs-kaist/taxi-front/graphs/contributor - frontend : https://github.com/sparcs-kaist/taxi-front - backend : https://github.com/sparcs-kaist/taxi-back - app : https://github.com/sparcs-kaist/taxi-app + - docker : https://github.com/sparcs-kaist/taxi-docker - figma : https://www.figma.com/file/li34hP1oStJAzLNjcG5KjN/SPARCS-Taxi?node-id=0%3A1 - taxiSampleGenerator : https://github.com/sparcs-kaist/taxiSampleGenerator diff --git a/package-lock.json b/package-lock.json index 2bef75f2..93a20f06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "express-session": "^1.17.1", "express-socket.io-session": "^1.3.5", "express-validator": "^6.14.0", + "jsonwebtoken": "^8.5.1", "mongoose": "^6.5.2", "querystring": "^0.2.1", "redis": "^4.2.0", @@ -3668,6 +3669,11 @@ "isarray": "^1.0.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4398,6 +4404,14 @@ "node": ">=12" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6185,6 +6199,54 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/jw-paginate": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/jw-paginate/-/jw-paginate-1.0.4.tgz", @@ -6265,6 +6327,36 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6275,6 +6367,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.reduce": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", @@ -11990,6 +12087,11 @@ "isarray": "^1.0.0" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -12510,6 +12612,14 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -13791,6 +13901,49 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "jw-paginate": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/jw-paginate/-/jw-paginate-1.0.4.tgz", @@ -13856,6 +14009,36 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -13866,6 +14049,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lodash.reduce": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", diff --git a/package.json b/package.json index f9b671fa..f6ecb4c8 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "express-session": "^1.17.1", "express-socket.io-session": "^1.3.5", "express-validator": "^6.14.0", + "jsonwebtoken": "^8.5.1", "mongoose": "^6.5.2", "querystring": "^0.2.1", "redis": "^4.2.0", diff --git a/security.js b/security.js index 32ecc65b..0511ffe6 100644 --- a/security.js +++ b/security.js @@ -16,4 +16,6 @@ module.exports = { secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, s3BucketName: process.env.AWS_S3_BUCKET_NAME, }, + jwtSecretKey: process.env.JWT_SECRET_KEY, + appUriScheme: process.env.APP_URI_SCHEME, }; diff --git a/src/config/constants.js b/src/config/constants.js new file mode 100644 index 00000000..2e8d139d --- /dev/null +++ b/src/config/constants.js @@ -0,0 +1,7 @@ +const TOKEN_EXPIRED = -3; +const TOKEN_INVALID = -2; + +module.exports = { + TOKEN_INVALID, + TOKEN_EXPIRED, +}; diff --git a/src/config/secretKey.js b/src/config/secretKey.js new file mode 100644 index 00000000..bbd50ce1 --- /dev/null +++ b/src/config/secretKey.js @@ -0,0 +1,9 @@ +const { jwtSecretKey, frontUrl } = require("../../security"); + +module.exports = { + jwtSecretKey: jwtSecretKey, + option: { + algorithm: "HS256", + issuer: frontUrl, + }, +}; diff --git a/src/db/mongo.js b/src/db/mongo.js index 874dc368..fffc6c89 100755 --- a/src/db/mongo.js +++ b/src/db/mongo.js @@ -35,6 +35,16 @@ const participantSchema = Schema({ }, }); +const deviceTokenSchema = Schema({ + userid: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + unique: true, + }, + deviceToken: [{ type: String, required: true }], +}); + const roomSchema = Schema({ name: { type: String, required: true, default: "이름 없음", text: true }, from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, @@ -110,6 +120,7 @@ mongoose.connect(security.mongo, { module.exports = { userModel: mongoose.model("User", userSchema), + deviceTokenModel: mongoose.model("DeviceToken", deviceTokenSchema), roomModel: mongoose.model("Room", roomSchema), locationModel: mongoose.model("Location", locationSchema), chatModel: mongoose.model("Chat", chatSchema), diff --git a/src/modules/jwt.js b/src/modules/jwt.js new file mode 100644 index 00000000..8931e724 --- /dev/null +++ b/src/modules/jwt.js @@ -0,0 +1,43 @@ +const jwt = require("jsonwebtoken"); +const { jwtSecretKey, option } = require("../config/secretKey"); +const { TOKEN_EXPIRED, TOKEN_INVALID } = require("../config/constants"); + +const signJwt = async ({ id, type }) => { + const payload = { + id: id, + type: type, + }; + + const options = { ...option }; + + if (type === "refresh") { + options.expiresIn = "30d"; + } + if (type === "access") { + options.expiresIn = "14d"; + } + + const result = { + token: jwt.sign(payload, jwtSecretKey, options), + }; + return result; +}; + +const verifyJwt = async (token) => { + let decoded; + try { + decoded = jwt.verify(token, jwtSecretKey); + } catch (err) { + if (err.message === "jwt expired") { + return TOKEN_EXPIRED; + } else { + return TOKEN_INVALID; + } + } + return decoded; +}; + +module.exports = { + sign: signJwt, + verify: verifyJwt, +}; diff --git a/src/route/auth.js b/src/route/auth.js index f43020cd..fc83f8b3 100644 --- a/src/route/auth.js +++ b/src/route/auth.js @@ -5,6 +5,7 @@ const authReplace = require("./auth.replace"); const router = express.Router(); const setTimestamp = require("../middleware/setTimestamp"); const authHandlers = require("../service/auth"); +const mobileAuthHandlers = require("../service/auth.mobile"); router.route("/sparcssso").get(authHandlers.sparcsssoHandler); router @@ -12,5 +13,11 @@ router .get(setTimestamp, authHandlers.sparcsssoCallbackHandler); router.route("/logout").get(authHandlers.logoutHandler); +router.route("/app/token/login").get(mobileAuthHandlers.loginWithToken); +router.route("/app/token/refresh").get(mobileAuthHandlers.refreshAccessToken); +router.route("/app/device").post(mobileAuthHandlers.registerDeviceTokenHandler); +router.route("/app/device").delete(mobileAuthHandlers.removeDeviceTokenHandler); +router.route("/app/token/generate").get(authHandlers.generateTokenHandler); + // 환경변수 SPARCSSSO_CLIENT_ID 유무에 따라 로그인 방식이 변경됩니다. module.exports = security.sparcssso?.id ? router : authReplace; diff --git a/src/route/docs/auth.md b/src/route/docs/auth.md index 6ef583a7..133d2017 100644 --- a/src/route/docs/auth.md +++ b/src/route/docs/auth.md @@ -78,3 +78,104 @@ #### Errors - 403 "not logged in" + +### `/app/token/generate` **(GET)** + +- SPARCSSSO로 로그인을 진행하고 로그인 정보를 담아 ACCESSTOKEN, REFRESHTOKEN을 반환 + +#### URL Parameters + +- None + +#### Response + +app's deep link +형식 APP_URI_SCHEME + ://login?accessToken=[ACCESSTOKEN]&refreshToken=[REFRESHTOKEN] + +#### Errors + +- 없음 + +### `/app/token/refresh` **(GET)** + +- 만료된 access token을 refresh token을 활용하여 갱신 + +#### URL Parameters + +- accessToken / 만료된 유효한 JWT Access Token이어야 함 +- refreshToken / 만료되지 않은 유효한 JWT Refresh Token 이어야 함. + +#### Response + +```javascript +{ + accessToken: [newAccessToken], // JSON Web Token + refreshToken: [newRefreshToken], //JSON Web Token +} +``` + +#### Errors + +- 401 / Invalid Access Token +- 401 / Invalid Token +- 401 / Expired Token +- 401 / Not Refresh Token +- 501 / Server Error + +### `/app/token/login` **(GET)** + +- access token을 사용하여 로그인 + +#### URL Parameters + +- accessToken / 만료 되지 않은 유효한 JWT accessToken 이어야 함 + +#### Response + +None / 세션 기록 + +#### Errors + +- 401 / Invalid Access Token +- 401 / Invalid Token +- 401 / Expired Token +- 401 / Not Refresh Token +- 501 / Server Error + +### `/app/device` **(POST)** + +- 기기의 deviceToken을 데이터베이스에 등록 + +#### URL Parameters + +- accessToken / 만료 되지 않은 유효한 JWT accessToken 이어야 함 +- deviceToken / Firebase 라이브러리에서 제공해주는 DeviceToken 이어야 함 + +#### Response + +None + +#### Errors + +- 400 / invalid request ( URL Parameters가 누락되어 있음 ) +- 401 / unauthorized ( 토큰이 유효하지 않음 ) +- 500 / server error + +### `/app/device` **(DELETE)** + +- 기기의 deviceToken을 데이터베이스에서 삭제 + +#### URL Parameters + +- accessToken / 만료 되지 않은 유효한 JWT accessToken 이어야 함 +- deviceToken / Firebase 라이브러리에서 제공해주는 DeviceToken 이어야 함 + +#### Response + +None + +#### Errors + +- 400 / invalid request ( URL Parameters가 누락되어 있음 ) +- 401 / unauthorized ( 토큰이 유효하지 않음 ) +- 500 / server error diff --git a/src/service/auth.js b/src/service/auth.js index 9155b718..4ebfb4b3 100644 --- a/src/service/auth.js +++ b/src/service/auth.js @@ -6,10 +6,15 @@ const { generateProfileImageUrl, getFullUsername, } = require("../modules/modifyProfile"); + +const jwt = require("../modules/jwt"); +const APP_URI_SCHEME = require("../../security").appUriScheme; + const { user: userPattern } = require("../db/patterns"); // SPARCS SSO const Client = require("../auth/sparcsso"); +const logger = require("../modules/logger"); const client = new Client(security.sparcssso?.id, security.sparcssso?.key); const transUserData = (userData) => { @@ -56,7 +61,6 @@ const joinus = (req, res, userData) => { }); }; -// 닉네임 변경? const update = async (req, res, userData) => { const updateInfo = { name: userData.name }; await userModel.updateOne({ id: userData.id }, updateInfo); @@ -83,6 +87,11 @@ const loginFalse = (req, res) => { res.redirect(security.frontUrl + "/login/false"); // 리엑트로 연결되나? }; +const generateTokenHandler = (req, res) => { + req.session.isApp = true; + sparcsssoHandler(req, res); +}; + const sparcsssoHandler = (req, res) => { const userInfo = getLoginInfo(req); const { url, state } = client.getLoginParams(); @@ -94,19 +103,55 @@ const sparcsssoCallbackHandler = (req, res) => { const state1 = req.session.state; const state2 = req.body.state || req.query.state; - if (state1 != state2) loginFalse(req, res); + if (state1 !== state2) loginFalse(req, res); else { const code = req.body.code || req.query.code; client.getUserInfo(code).then((userDataBefore) => { const userData = transUserData(userDataBefore); - // 로그인 시마다 사용자가 KAIST 구성원인지 검증함. - if (userData.isEligible || security.nodeEnv !== "production") - loginDone(req, res, userData); - else loginFalse(req, res); + if (userData.isEligible || security.nodeEnv !== "production") { + if (req.session.isApp) { + createNewTokenHandler(req, res, userData); + } else { + loginDone(req, res, userData); + } + } else loginFalse(req, res); }); } }; +const createNewTokenHandler = (req, res, userData) => { + userModel.findOne( + { id: userData.id }, + "name id withdraw ban", + async (err, result) => { + if (err) { + logger.error(err); + loginFalse(req, res); + } else if (!result) joinus(req, res, userData); + else if (result.name !== userData.name) update(req, res, userData); + else { + const accessToken = await jwt.sign({ + id: result._id, + deviceToken: req.body.deviceToken, + type: "access", + }); + const refreshToken = await jwt.sign({ + id: result._id, + deviceToken: req.body.deviceToken, + type: "refresh", + }); + res.redirect( + APP_URI_SCHEME + + "://login?accessToken=" + + accessToken.token + + "&refreshToken=" + + refreshToken.token + ); + } + } + ); +}; + const logoutHandler = (req, res) => { logout(req, res); res.status(200).send("logged out successfully"); @@ -116,4 +161,5 @@ module.exports = { sparcsssoHandler, sparcsssoCallbackHandler, logoutHandler, + generateTokenHandler, }; diff --git a/src/service/auth.mobile.js b/src/service/auth.mobile.js new file mode 100644 index 00000000..d1c8aaa4 --- /dev/null +++ b/src/service/auth.mobile.js @@ -0,0 +1,145 @@ +const { userModel } = require("../db/mongo"); +const { deviceTokenModel } = require("../db/mongo"); +const { login } = require("../auth/login"); + +const jwt = require("../modules/jwt"); + +const { TOKEN_EXPIRED, TOKEN_INVALID } = require("../config/constants"); + +const loginWithToken = async (req, res) => { + req.session.isApp = true; + const { accessToken, deviceToken } = req.query; + try { + if (!accessToken || !deviceToken) + return res.status(400).send("invalid request"); + const data = await jwt.verify(accessToken); + + if (data === TOKEN_INVALID) { + return res.status(401).json({ message: "Invalid token" }); + } + + if (data === TOKEN_EXPIRED) { + return res.status(401).json({ message: "Expired token" }); + } + + if (data.type !== "access") { + return res.status(401).json({ message: "Not Access token" }); + } + + const userInfo = await userModel.findOne({ _id: data.id }); + + if (!userInfo) + return res.status(401).json({ message: "No corresponding user" }); + else { + login(req, userInfo.sid, userInfo.id, userInfo.name); + req.session.deviceToken = deviceToken; + return res.status(200).json({ message: "success" }); + } + } catch (e) { + logger.error(e); + return res.status(500).send("server error"); + } +}; + +const refreshAccessToken = async (req, res) => { + const { accessToken, refreshToken } = req.query; + if (!accessToken || !refreshToken) + return res.status(400).send("invalid request"); + + try { + const data = await jwt.verify(refreshToken); + + const accessTokenStatus = await jwt.verify(accessToken); + + if (accessTokenStatus === TOKEN_INVALID) { + return res.status(401).json({ message: "Invalid access token" }); + } + + if (data === TOKEN_INVALID) { + return res.status(401).json({ message: "Invalid token" }); + } + + if (data === TOKEN_EXPIRED) { + return res.status(401).json({ message: "Expired token" }); + } + + if (data.type !== "refresh") { + return res.status(401).json({ message: "Not Refresh token" }); + } + + const newAccessToken = await jwt.sign({ id: data.id, type: "access" }); + const newRefreshToken = await jwt.sign({ id: data.id, type: "refresh" }); + return res.json({ + accessToken: newAccessToken.token, + refreshToken: newRefreshToken.token, + }); + } catch (e) { + logger.error(e); + res.status(501).send("server error"); + } +}; + +const registerDeviceTokenHandler = async (req, res) => { + try { + const { accessToken, deviceToken } = req.body; + + const accessTokenStatus = await jwt.verify(accessToken); + + if (!deviceToken) return res.status(400).send("invalid request"); + if ( + accessTokenStatus === TOKEN_EXPIRED || + accessTokenStatus === TOKEN_INVALID + ) + return res.status(401).send("unauthorized"); + + await deviceTokenModel.updateOne( + { + userid: accessTokenStatus.id, + }, + { + userid: accessTokenStatus.id, + $addToSet: { deviceToken: deviceToken }, + }, + { upsert: true, new: true } + ); + res.status(200).send("success"); + } catch (e) { + logger.error(e); + res.status(500).send("server error"); + } +}; + +const removeDeviceTokenHandler = async (req, res) => { + try { + const { accessToken, deviceToken } = req.body; + + const accessTokenStatus = await jwt.verify(accessToken); + + if (!deviceToken) return res.status(400).send("invalid request"); + + if ( + accessTokenStatus === TOKEN_EXPIRED || + accessTokenStatus === TOKEN_INVALID + ) + return res.status(401).send("unauthorized"); + + await deviceTokenModel.updateOne( + { + userid: accessTokenStatus.id, + }, + { userid: accessTokenStatus.id, $pull: { deviceToken: deviceToken } }, + { upsert: true, new: true } + ); + res.status(200).send("success"); + } catch (e) { + logger.error(e); + res.status(500).send("server error"); + } +}; + +module.exports = { + loginWithToken, + refreshAccessToken, + registerDeviceTokenHandler, + removeDeviceTokenHandler, +};