diff --git a/package-lock.json b/package-lock.json index d907518..67afd7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.9.1", + "@types/jsonwebtoken": "^9.0.5", "dotenv": "^16.4.1", "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", - "fastify-schema-to-ts": "^1.0.1" + "fastify-schema-to-ts": "^1.0.1", + "jsonwebtoken": "^9.0.2" }, "devDependencies": { "@types/node": "^20.11.16", @@ -498,11 +500,18 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "peer": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.11.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -619,6 +628,11 @@ "ieee754": "^1.2.1" } }, + "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/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -654,6 +668,14 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "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/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", @@ -914,6 +936,46 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "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": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "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/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/light-my-request": { "version": "5.11.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", @@ -929,6 +991,41 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" }, + "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.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1250,8 +1347,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/uri-js": { "version": "4.4.1", diff --git a/package.json b/package.json index 8f4f1af..e4ba468 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,12 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.9.1", + "@types/jsonwebtoken": "^9.0.5", "dotenv": "^16.4.1", "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", - "fastify-schema-to-ts": "^1.0.1" + "fastify-schema-to-ts": "^1.0.1", + "jsonwebtoken": "^9.0.2" }, "devDependencies": { "@types/node": "^20.11.16", diff --git a/src/DTO/index.dto.ts b/src/DTO/index.dto.ts new file mode 100644 index 0000000..4a016d9 --- /dev/null +++ b/src/DTO/index.dto.ts @@ -0,0 +1,65 @@ +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import ErrorConfig from '@errors/config'; +import { ErrorWithToast } from '@errors'; +export const AuthorizationHeader = { + type: 'object', + properties: { + authorization: { type: 'string' }, + }, + required: ['authorization'], +} as const; + +export const StoreAuthorizationHeader = { + type: 'object', + properties: { + authorization: { type: 'string' }, + storeid: { type: 'string' }, + }, + required: ['authorization', 'storeid'], +} as const; + +export const errorSchema = (...errors: Array ErrorWithToast>) => { + const errorConfigs = ErrorConfig.filter((errorConfig) => errors.some((error) => errorConfig.error === error)); + return errorConfigs.reduce((acc, cur) => { + const errorInstance = new cur.error(""); + if(acc[cur.code]) { + acc[cur.code].properties.error.enum.push(errorInstance.name); + acc[cur.code].description += `\n${cur.describtion}`; + acc[cur.code].properties.toast.enum.push(cur.toast(errorInstance)); + return acc; + } + acc[cur.code] = { + type: 'object', + description: cur.describtion, + required: ['error','message','toast'], + properties: { + error: { type: 'string', enum: [errorInstance.name] }, + message: { type: 'string'}, + toast: { type: 'string', enum: [cur.toast(errorInstance)] } + } + } + return acc; + },{} as { + [key:number]: { + type: 'object', + description: string, + required: ['error','message','toast'], + properties: { + error: { type: 'string', enum: string[] }, + message: { type: 'string'}, + toast: { type: 'string', enum: string[] } + } + } + }); +}; + +export type ErrorInterface = FromSchema<{ + type: 'object', + description: string, + required: ['error','message','toast'], + properties: { + error: { type: 'string', enum: string[] }, + message: { type: 'string'}, + toast: { type: 'string', enum: string[] } + } +}>; diff --git a/src/api/hooks/checkUser.ts b/src/api/hooks/checkUser.ts new file mode 100644 index 0000000..516591f --- /dev/null +++ b/src/api/hooks/checkUser.ts @@ -0,0 +1,20 @@ +import { LoginToken } from '@utils/jwt'; +import { FastifyRequest, FastifyReply, FastifyError } from 'fastify'; +import { UserAuthorizationError, NoAuthorizationInHeaderError } from '@errors/index'; + +export default async ( + request: FastifyRequest<{ Body: { userId: number } }>, + reply: FastifyReply, + done: (err?: FastifyError) => void +) => { + const authorization = request.headers.authorization; + if (!authorization) { + throw new NoAuthorizationInHeaderError('헤더에 Authorization이 없습니다'); + } + + const replace_authorization = authorization.replace('Bearer ', ''); + + if(!request.body) + request.body = { userId: 0 }; + request.body.userId = LoginToken.getUserId(replace_authorization); +}; diff --git a/src/api/hooks/onError.ts b/src/api/hooks/onError.ts new file mode 100644 index 0000000..0695667 --- /dev/null +++ b/src/api/hooks/onError.ts @@ -0,0 +1,39 @@ +import { FastifyRequest, FastifyReply, FastifyError } from 'fastify'; +import { ErrorWithToast, ValidationError } from '@errors'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import ErrorConfig from '@errors/config'; + +export default ( + request: FastifyRequest, + reply: FastifyReply, + error: FastifyError & { toast?: string } +) => { + if (!(error instanceof ErrorWithToast)) { + if (error.validation) { + error.toast = ErrorConfig.find( + (config) => config.error === ValidationError + )!.toast(error); + return reply.code(400).send(error); + } + if(error as FastifyError & { toast?: string } instanceof PrismaClientKnownRequestError) { + if(error.code === 'P2025') { + error.toast = '찾을 수 없는 데이터가 포함되어 있습니다.'; + return reply.code(404).send(error); + } + error.toast = '잘못된 데이터가 입력되었습니다.'; + return reply.code(400).send(error); + } + error.toast = '알 수 없는 에러가 발생했습니다.'; + return reply.code(500).send(error); + } + const knownError = ErrorConfig.find( + (config) => error instanceof config.error + ); + if (knownError) { + return reply + .code(knownError.code) + .send(error.setToast(knownError.toast(error))); + } + + reply.code(500).send(); +}; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..6d7ef40 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,8 @@ +import { FastifyInstance, FastifyPluginAsync, FastifySchema } from 'fastify'; +import test from './routes/apiTest'; + +const api: FastifyPluginAsync = async (server: FastifyInstance) => { + server.register(test, { prefix: '/' }); +}; + +export default api; diff --git a/src/api/routes/apiTest.ts b/src/api/routes/apiTest.ts new file mode 100644 index 0000000..9ee7fbb --- /dev/null +++ b/src/api/routes/apiTest.ts @@ -0,0 +1,26 @@ +import { FastifyInstance, FastifyPluginAsync, FastifySchema } from 'fastify'; +import onError from '@hooks/onError'; +import { NotDefinedOnConfigError } from '@errors/index'; + +const test: FastifyPluginAsync = async (server: FastifyInstance) => { + const testSchema: FastifySchema = { + response: { + 200: { + type: 'object', + properties: { + data: { type: 'string' }, + }, + }, + }, + }; + server.get('/ping', { schema: testSchema }, async (req, rep) => { + return { data: 'pong' }; + }); + server.post('/notDefinedOnConfigerror', { onError }, async (req, rep) => { + throw new NotDefinedOnConfigError('notDefinedOnConfigerror'); + }); + server.post('/notDefinederror', { onError }, async (req, rep) => { + throw new Error('notDefinederror'); + }); +}; +export default test; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..75a471b --- /dev/null +++ b/src/config.ts @@ -0,0 +1,27 @@ +import { config } from 'dotenv'; + +class Config{ + static instance: Config|null=null; + constructor() {} + + static of(): Config { + if (!Config.instance) { + config(); + process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + Config.instance = new Config(); + } + return Config.instance; + } + + config() { + return { + port: parseInt(process.env.NODE_ENV == 'test' ? '3001' : process.env.PORT || '3000'), + jwtSecretKey: process.env.JWT_SECRET_KEY || 'secretKey', + salt: process.env.SALT || 'salt', + nodeEnv: process.env.NODE_ENV, + } as const; + } + +} + +export default Config.of().config(); diff --git a/src/errors/config.ts b/src/errors/config.ts new file mode 100644 index 0000000..7a319e1 --- /dev/null +++ b/src/errors/config.ts @@ -0,0 +1,72 @@ +import * as E from '@errors'; +export type ErrorConfigType = { + error: new (message: string, ...any: any) => E.ErrorWithToast; + code: number; + toast: (error: any) => string; + describtion: string; +}[]; +const ErrorConfig: ErrorConfigType = [ + { + describtion: '요청이 잘못되었습니다.', + error: E.ValidationError, + code: 400, + toast: (error: E.ErrorWithToast) => `누락된 정보가 있습니다.`, + }, + { + describtion: '요청은 성공했으나, 응답을 제공할 데이터가 없습니다.', + error: E.NotFoundError, + code: 404, + toast: (error: E.NotFoundError) => + `${error.missing}을(를) 찾을 수 없습니다.`, + }, + { + describtion: '토큰이 만료되었거나, 유효하지 않습니다.', + error: E.UserAuthorizationError, + code: 401, + toast: (error: E.ErrorWithToast) => `다시 로그인 해주세요.`, + }, + { + describtion: '사용자가 소유한 가게가 아닙니다.', + error: E.StoreAuthorizationError, + code: 401, + toast: (error: E.ErrorWithToast) => `가게 접근 권한이 없습니다.`, + }, + { + describtion: '헤더에 인증 정보가 없습니다.', + error: E.NoAuthorizationInHeaderError, + code: 400, + toast: (error: E.ErrorWithToast) => `인증 정보가 없습니다.`, + }, + { + describtion: '입력된 값이 양식에 맞지 않습니다.', + error: E.NotCorrectTypeError, + code: 400, + toast: (error: E.NotCorrectTypeError) => + `입력된 ${error.unCorrect}이(가) 양식과 맞지 않습니다.`, + }, + { + describtion: '이미 등록된 값이 있습니다.', + error: E.ExistError, + code: 409, + toast: (error: E.ExistError) => `입력된 ${error.exist}가 이미 존재합니다.`, + }, + { + describtion: '잔액이 부족합니다.', + error: E.NotEnoughError, + code: 403, + toast: (error: E.ErrorWithToast) => `잔액이 부족합니다.`, + }, + { + describtion: '토큰이 유효하지 않습니다.', + error: E.UncorrectTokenError, + code: 403, + toast: (error: E.ErrorWithToast) => '토큰이 유효하지 않습니다.', + }, + { + describtion: '이미 결제된 주문입니다.', + error: E.AlreadyPaidError, + code: 403, + toast: (error: E.ErrorWithToast) => '이미 결제된 주문입니다.', + } +]; +export default ErrorConfig; diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..06e571e --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,68 @@ +export class ErrorWithToast extends Error { + name: string = 'UnknownError'; + toast: string = '알 수 없는 에러가 발생했습니다.'; + constructor(message: string) { + super(message); + } + setToast(toast: string): ErrorWithToast { + this.toast = toast; + return this; + } +} + +const generateGenericError = (name: string) => { + return class extends ErrorWithToast { + constructor(message: string) { + super(message); + this.name = name; + } + }; +}; + +export class NotFoundError extends ErrorWithToast { + missing: string; + constructor(message: string, missing: string = '{missing}') { + super(message); + this.name = 'NotFoundError'; + this.missing = missing; + } +} + +export class NotCorrectTypeError extends ErrorWithToast { + unCorrect: string; + constructor(message: string, unCorrect: string = '{unCorrect}') { + super(message); + this.name = 'NotCorrectTypeError'; + this.unCorrect = unCorrect; + } +} +export class ExistError extends ErrorWithToast { + exist: string; + constructor(message: string, exist: string = '{exist}') { + super(message); + this.name = 'ExistError'; + this.exist = exist; + } +} + +export const AlreadyPaidError = generateGenericError('alreadyPaidError'); + +export const NotEnoughError = generateGenericError('NotEnoughError'); + +export const UncorrectTokenError = generateGenericError('UncorrectTokenError'); + +export const NotDefinedOnConfigError = generateGenericError("NotDefinedOnConfigError"); + +export const ValidationError = generateGenericError('ValidationError'); + +export const UserAuthorizationError = generateGenericError( + 'UserAuthorizationError' +); + +export const StoreAuthorizationError = generateGenericError( + 'StoreAuthorizationError' +); + +export const NoAuthorizationInHeaderError = generateGenericError( + 'NoAuthorizationInHeaderError' +); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..80e65fb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,18 @@ +import {FastifyInstance} from 'fastify'; +import serverSetting from '@server'; +import config from '@config'; + + +const startServer = async (server : FastifyInstance) => { + try { + await server.listen({port: config.port, host: '0.0.0.0'}) + } catch (err) { + server.log.error(err); + process.exit(1); + } +} + +(async () => { + const settedServer:FastifyInstance = await serverSetting(); + startServer(settedServer); +})(); diff --git a/src/loaders/index.ts b/src/loaders/index.ts new file mode 100644 index 0000000..86c4fbe --- /dev/null +++ b/src/loaders/index.ts @@ -0,0 +1,6 @@ +import { FastifyInstance } from 'fastify'; +import api from '@api'; + +export default async (server: FastifyInstance): Promise => { + server.register(api, { prefix: '/api/v1' }); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..520232b --- /dev/null +++ b/src/server.ts @@ -0,0 +1,10 @@ +import fastify, {FastifyInstance} from 'fastify'; +import loaders from '@loaders'; + +const serverSetting = async () : Promise => { + const server = fastify({logger: process.env.NODE_ENV === 'development'}); + await loaders(server); + return server; +} + +export default serverSetting; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 0000000..71bdb6a --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,96 @@ +import jwt from 'jsonwebtoken'; +import config from '@config'; +import crypto from 'crypto'; +import { UserAuthorizationError, UncorrectTokenError } from '@errors'; + +export class TokenForCertificatePhone { + phone: string; + encryptedCertificationCode: string; + + constructor(phone: string, certificationCode: string) { + this.phone = phone; + this.encryptedCertificationCode = this.encryptCertificationCode( + phone, + certificationCode + ); + } + + private encryptCertificationCode( + phone: string, + certificationCode: string + ): string { + const hash = crypto.createHash('sha512'); + hash.update(`${phone}${certificationCode}${config.salt}`); + return hash.digest('hex'); + } + + sign(): string { + return jwt.sign({ ...this }, config.jwtSecretKey, { expiresIn: '3m' }); + } + + public static verify( + token: string, + phone: string, + certificationCode: string + ): boolean { + const decoded = jwt.verify( + token, + config.jwtSecretKey + ) as TokenForCertificatePhone; + const hash = crypto.createHash('sha512'); + hash.update(`${phone}${certificationCode}${config.salt}`); + const encryptedCertificationCode = hash.digest('hex'); + if (decoded.encryptedCertificationCode == encryptedCertificationCode) + return true; + return false; + } +} + +export class CertificatedPhoneToken { + phone: string; + + constructor(phone: string) { + this.phone = phone; + } + + sign(): string { + return jwt.sign({ ...this }, config.jwtSecretKey, { expiresIn: '3m' }); + } + + public static decode(token: string): CertificatedPhoneToken { + try { + const decoded = jwt.verify( + token, + config.jwtSecretKey + ) as CertificatedPhoneToken; + return decoded; + } catch (err) { + throw new UncorrectTokenError('토큰이 유효하지 않습니다.'); + } + } +} + +export class LoginToken { + userId: number; + + constructor(userId: number) { + this.userId = userId; + } + + signAccessToken(): string { + return jwt.sign({ ...this }, config.jwtSecretKey, { expiresIn: '1d' }); + } + + signRefreshToken(): string { + return jwt.sign({ ...this }, config.jwtSecretKey, { expiresIn: '14d' }); + } + + public static getUserId(token: string): number { + try { + const decoded = jwt.verify(token, config.jwtSecretKey) as LoginToken; + return decoded.userId; + } catch (err) { + throw new UserAuthorizationError('유저 인증에 실패했습니다'); + } + } +} diff --git a/tsconfig.json b/tsconfig.json index bed84c9..138e5e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,17 @@ "esModuleInterop": true, "baseUrl": ".", "paths": { - "@/*": ["src/*"], + "@api": ["src/api"], + "@config": ["src/config"], + "@DTO/*": ["src/DTO/*"], + "@loaders": ["src/loaders"], + "@services/*": ["src/services/*"], + "@utils/*": ["src/utils/*"], + "@errors": ["src/errors"], + "@errors/*": ["src/errors/*"], + "@routes/*": ["src/api/routes/*"], + "@hooks/*": ["src/api/hooks/*"], + "@server": ["src/server"], } }, "include": [