Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6th seminar #6

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion seminar4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@
"license": "MIT",
"scripts": {
"dev": "nodemon",
"build": "tsc && node dist"
"build": "tsc && node dist",
"db:pull": "npx prisma db pull",
"db:push": "npx prisma db push",
"generate": "npx prisma generate"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/express": "^4.17.14",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.9",
"nodemon": "^2.0.20"
},
"dependencies": {
"@prisma/client": "^4.5.0",
"@types/express-validator": "^3.0.0",
"bcryptjs": "^2.4.3",
"express": "^4.18.2",
"express-validator": "^6.14.2",
"jsonwebtoken": "^8.5.1",
"prisma": "^4.5.0"
}
}
9 changes: 5 additions & 4 deletions seminar4/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ datasource db {
}

model User {
id Int @id(map: "user_pk") @unique(map: "user_id_uindex") @default(autoincrement())
userName String
id Int @id(map: "user_pk") @unique(map: "user_id_uindex") @default(autoincrement())
userName String @db.VarChar(100)
age Int?
email String? @db.VarChar(400)
}
email String @db.VarChar(400)
password String @db.VarChar(400)
}
3 changes: 3 additions & 0 deletions seminar4/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as rm } from './responseMessage';
export { default as sc } from './statusCode';
export { default as tokenType } from './tokenType';
16 changes: 16 additions & 0 deletions seminar4/src/constants/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const success = (status: number, message: string, data?: any) => {
return {
status,
success: true,
message,
data,
};
};

export const fail = (status: number, message: string) => {
return {
status,
success: false,
message,
};
};
33 changes: 33 additions & 0 deletions seminar4/src/constants/responseMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export default {
NULL_VALUE: 'ํ•„์š”ํ•œ ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค.',
OUT_OF_VALUE: 'ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
NOT_FOUND: '์ž˜๋ชป๋œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.',
BAD_REQUEST: '์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.',

// ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ
SIGNUP_SUCCESS: 'ํšŒ์› ๊ฐ€์ž… ์„ฑ๊ณต',
SIGNUP_FAIL: 'ํšŒ์› ๊ฐ€์ž… ์‹คํŒจ',
SIGNIN_SUCCESS: '๋กœ๊ทธ์ธ ์„ฑ๊ณต',
SIGNIN_FAIL: '๋กœ๊ทธ์ธ ์‹คํŒจ',
ALREADY_NICKNAME: '์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค.',

// ์œ ์ €
READ_USER_SUCCESS: '์œ ์ € ์กฐํšŒ ์„ฑ๊ณต',
READ_ALL_USERS_SUCCESS: '๋ชจ๋“  ์œ ์ € ์กฐํšŒ ์„ฑ๊ณต',
UPDATE_USER_SUCCESS: '์œ ์ € ์ˆ˜์ • ์„ฑ๊ณต',
DELETE_USER_SUCCESS: '์œ ์ € ํƒˆํ‡ด ์„ฑ๊ณต',
DELETE_USER_FAIL: '์œ ์ € ํƒˆํ‡ด ์‹คํŒจ',
NO_USER: 'ํƒˆํ‡ดํ–ˆ๊ฑฐ๋‚˜ ๊ฐ€์ž…ํ•˜์ง€ ์•Š์€ ์œ ์ €์ž…๋‹ˆ๋‹ค.',

// ํ† ํฐ
CREATE_TOKEN_SUCCESS: 'ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต',
EXPIRED_TOKEN: 'ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
EXPIRED_ALL_TOKEN: '๋ชจ๋“  ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
INVALID_TOKEN: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.',
VALID_TOKEN: '์œ ํšจํ•œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.',
EMPTY_TOKEN: 'ํ† ํฐ ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค.',

// ์„œ๋ฒ„ ๋‚ด ์˜ค๋ฅ˜
INTERNAL_SERVER_ERROR: '์„œ๋ฒ„ ๋‚ด ์˜ค๋ฅ˜',
INVALID_PASSWORD: '์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค.',
};
15 changes: 15 additions & 0 deletions seminar4/src/constants/statusCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
OK: 200, // ๋ชฉ๋ก, ์ƒ์„ธ, ์ˆ˜์ • ์„ฑ๊ณต
CREATED: 201, // POST๋‚˜ PUT์œผ๋กœ ๋ฐ์ดํ„ฐ ๋“ฑ๋กํ•  ๊ฒฝ์šฐ ์‚ฌ์šฉ
// ์–ด๋– ํ•œ ์ƒ์„ฑ ์ž‘์—…์„ ์š”์ฒญ๋ฐ›์•„ ์ƒ์„ฑ ์ž‘์—…์„ ์„ฑ๊ณต
ACCEPTED: 202, // ์š”์ฒญ์€ ๋ฐ›์•˜์ง€๋งŒ, ์•„์ง ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์š”์ฒญ์ด ์ ์ ˆํ•จ์„ ์˜๋ฏธํ•œ๋‹ค.
NO_CONTENT: 204, // ์š”์ฒญ์ด ์„ฑ๊ณต์€ ํ–ˆ์ง€๋งŒ ์‘๋‹ตํ•  ์ฝ˜ํ…์ธ ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ๋ฅผ ๋œปํ•œ๋‹ค.
BAD_REQUEST: 400, // ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ๋ชปํ•œ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์žˆ์Œ์„ ์˜๋ฏธ
UNAUTHORIZED: 401, // ๋กœ๊ทธ์ธ์„ ํ•˜์ง€ ์•Š์•„ ๊ถŒํ•œ์ด ์—†์Œ. ๊ถŒํ•œ ์ธ์ฆ ์š”๊ตฌ
FORBIDDEN: 403, // ์š”์ฒญ์ด ์„œ๋ฒ„์— ์˜ํ•ด ๊ฑฐ๋ถ€ ๋˜์—ˆ์Œ์„ ์˜๋ฏธ. ๊ธˆ์ง€๋œ ํŽ˜์ด์ง€
NOT_FOUND: 404, // ์š”์ฒญํ•œ URL์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ์„ ์˜๋ฏธ
CONFLICT: 409, // ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์— ๋Œ€ํ•ด ์„œ๋ฒ„์—์„œ ์ถฉ๋Œ ์š”์†Œ๊ฐ€ ๋ฐœ์ƒ ํ• ์ˆ˜ ์žˆ์Œ์„ ์˜๋ฏธ
INTERNAL_SERVER_ERROR: 500, // ์„œ๋ฒ„์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ์‘๋‹ต ํ•  ์ˆ˜ ์—†์Œ์„ ์˜๋ฏธ
SERVICE_UNAVAILABLE: 503, // ํ˜„์žฌ ์„œ๋ฒ„๊ฐ€ ์œ ์ง€๋ณด์ˆ˜ ๋“ฑ์˜ ์ด์œ ๋กœ ์ผ์‹œ์ ์ธ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ํ•จ์„ ์˜๋ฏธ
DB_ERROR: 600,
};
4 changes: 4 additions & 0 deletions seminar4/src/constants/tokenType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
TOKEN_EXPIRED: -3,
TOKEN_INVALID: -2,
};
67 changes: 63 additions & 4 deletions seminar4/src/controller/userController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Request, Response } from 'express';
import { validationResult } from 'express-validator';
import { rm, sc } from '../constants';
import { fail, success } from '../constants/response';
import { UserCreateDTO } from '../DTO/userDTO';
import { UserSignInDTO } from '../interfaces/UserSignInDTO';
import jwtHandler from '../modules/jwtHandler';
import { userService } from '../service';

const getUserById = async (req: Request, res: Response) => {
Expand All @@ -13,14 +18,37 @@ const getUserById = async (req: Request, res: Response) => {
return res.status(200).json({ status: 200, message: '์œ ์ € ์กฐํšŒ ์„ฑ๊ณต', data });
};

// const createUser = async (req: Request, res: Response) => {
// const userCreateDTO: UserCreateDTO = req.body;
// const data = await userService.createUser(userCreateDTO);

// if (!data) {
// return res.status(404).json({ status: 404, message: 'NOT_FOUND' });
// }
// return res.status(200).json({ status: 200, message: '์œ ์ € ์ƒ์„ฑ ์„ฑ๊ณต', data });
// };

const createUser = async (req: Request, res: Response) => {
const userCreateDTO: UserCreateDTO = req.body;
const data = await userService.createUser(userCreateDTO);
//? validation์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
const error = validationResult(req);
if (!error.isEmpty()) {
return res.status(sc.BAD_REQUEST).send(fail(sc.BAD_REQUEST, rm.BAD_REQUEST));
}
//? ๊ธฐ์กด ๋น„๊ตฌ์กฐํ™” ํ• ๋‹น ๋ฐฉ์‹ -> DTO์˜ ํ˜•ํƒœ
const userCreateDto: UserCreateDTO = req.body;
const data = await userService.createUser(userCreateDto);

if (!data) {
return res.status(404).json({ status: 404, message: 'NOT_FOUND' });
return res.status(sc.BAD_REQUEST).send(fail(sc.BAD_REQUEST, rm.SIGNUP_FAIL));
}
return res.status(200).json({ status: 200, message: '์œ ์ € ์ƒ์„ฑ ์„ฑ๊ณต', data });
const accessToken = jwtHandler.sign(data.id);

const result = {
id: data.id,
name: data.userName,
accessToken,
};
return res.status(sc.CREATED).send(success(sc.CREATED, rm.SIGNUP_SUCCESS, data));
};

const getAllUser = async (req: Request, res: Response) => {
Expand Down Expand Up @@ -54,12 +82,43 @@ const deleteUser = async (req: Request, res: Response) => {
}
return res.status(200).json({ status: 200, message: '์œ ์ € ์‚ญ์ œ ์„ฑ๊ณต', data });
};

const signInUser = async (req: Request, res: Response) => {
const error = validationResult(req);
if (!error.isEmpty()) {
return res.status(sc.BAD_REQUEST).send(fail(sc.BAD_REQUEST, rm.BAD_REQUEST));
}

const userSignInDto: UserSignInDTO = req.body;

try {
const userId = await userService.signIn(userSignInDto);

if (!userId) return res.status(sc.NOT_FOUND).send(fail(sc.NOT_FOUND, rm.NOT_FOUND));
else if (userId === sc.UNAUTHORIZED) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.INVALID_PASSWORD));

const accessToken = jwtHandler.sign(userId);

const result = {
id: userId,
accessToken,
};

res.status(sc.OK).send(success(sc.OK, rm.SIGNIN_SUCCESS, result));
} catch (e) {
console.log(error);
//? ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ
res.status(sc.INTERNAL_SERVER_ERROR).send(fail(sc.INTERNAL_SERVER_ERROR, rm.INTERNAL_SERVER_ERROR));
}
};

const userController = {
getUserById,
createUser,
getAllUser,
updateUser,
deleteUser,
signInUser,
};

export default userController;
6 changes: 6 additions & 0 deletions seminar4/src/interfaces/UserCreateDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface UserCreateDTO {
name: string;
age: number;
email: string;
password: string;
}
4 changes: 4 additions & 0 deletions seminar4/src/interfaces/UserSignInDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface UserSignInDTO {
email: string;
password: string;
}
30 changes: 30 additions & 0 deletions seminar4/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextFunction, Request, Response } from 'express';
import { JwtPayload } from 'jsonwebtoken';
import { rm, sc } from '../constants';
import { fail } from '../constants/response';
import tokenType from '../constants/tokenType';
import jwtHandler from '../modules/jwtHandler';

export default async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(' ').reverse()[0]; //? Bearer ~~ ์—์„œ ํ† ํฐ๋งŒ ํŒŒ์‹ฑ
if (!token) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.EMPTY_TOKEN));

try {
const decoded = jwtHandler.verify(token); //? jwtHandler์—์„œ ๋งŒ๋“ค์–ด๋‘” verify๋กœ ํ† ํฐ ๊ฒ€์‚ฌ

//? ํ† ํฐ ์—๋Ÿฌ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
if (decoded === tokenType.TOKEN_EXPIRED) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.EXPIRED_TOKEN));
if (decoded === tokenType.TOKEN_INVALID) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.INVALID_TOKEN));

//? decodeํ•œ ํ›„ ๋‹ด๊ฒจ์žˆ๋Š” userId๋ฅผ ๊บผ๋‚ด์˜ด
const userId: number = (decoded as JwtPayload).userId;
if (!userId) return res.status(sc.UNAUTHORIZED).send(fail(sc.UNAUTHORIZED, rm.INVALID_TOKEN));

//? ์–ป์–ด๋‚ธ userId ๋ฅผ Request Body ๋‚ด userId ํ•„๋“œ์— ๋‹ด๊ณ , ๋‹ค์Œ ๋ฏธ๋“ค์›จ์–ด๋กœ ๋„˜๊น€( next() )
req.body.userId = userId;
next();
} catch (error) {
console.log(error);
res.status(sc.INTERNAL_SERVER_ERROR).send(fail(sc.INTERNAL_SERVER_ERROR, rm.INTERNAL_SERVER_ERROR));
}
};
1 change: 1 addition & 0 deletions seminar4/src/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as auth } from './auth';
37 changes: 37 additions & 0 deletions seminar4/src/modules/jwtHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// src/modules/jwtHandler.ts
import jwt from 'jsonwebtoken';
import { tokenType } from '../constants';

//* ๋ฐ›์•„์˜จ userId๋ฅผ ๋‹ด๋Š” access token ์ƒ์„ฑ
const sign = (userId: number) => {
const payload = {
userId,
};

const accessToken = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '2h' });
return accessToken;
};

//* token ๊ฒ€์‚ฌ!
const verify = (token: string) => {
let decoded: string | jwt.JwtPayload;

try {
decoded = jwt.verify(token, process.env.JWT_SECRET as string);
} catch (error: any) {
if (error.message === 'jwt expired') {
return tokenType.TOKEN_EXPIRED;
} else if (error.message === 'invalid token') {
return tokenType.TOKEN_INVALID;
} else {
return tokenType.TOKEN_INVALID;
}
}

return decoded;
};

export default {
sign,
verify,
};
7 changes: 4 additions & 3 deletions seminar4/src/router/userRouter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Router } from 'express';
import { userController } from '../controller';
import { body } from 'express-validator';
import { auth } from '../middlewares';
const { validatorErrorChecker } = require('../middlewares/validator');
const router: Router = Router();

router.get('/:userId', userController.getUserById);
router.get('/:userId', auth, userController.getUserById);

router.post('/', body('name').notEmpty(), validatorErrorChecker, userController.createUser);
router.post('/', [body('name').notEmpty(), body('email').notEmpty(), body('password').isLength({ min: 6 })], userController.createUser);
router.get('/', userController.getAllUser);
router.patch('/:userId', body('name').notEmpty(), validatorErrorChecker, userController.updateUser);
router.delete('/:userId', userController.deleteUser);

router.post('/signin', [body('email').notEmpty(), body('email').isEmail(), body('password').notEmpty(), body('password').isLength({ min: 6 })], userController.signInUser);
export default router;
54 changes: 48 additions & 6 deletions seminar4/src/service/userService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { UserCreateDTO } from '../DTO/userDTO';
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { sc } from '../constants';
import { UserSignInDTO } from '../interfaces/UserSignInDTO';
const prisma = new PrismaClient();

//* userId๋กœ ์œ ์ € ์กฐํšŒ
Expand All @@ -13,15 +16,53 @@ const getUserById = async (userId: number) => {
return user;
};

const createUser = async (userCreateDTO: UserCreateDTO) => {
const user = await prisma.user.create({
const signIn = async (userSignInDto: UserSignInDTO) => {
try {
const user = await prisma.user.findFirst({
where: {
email: userSignInDto.email,
},
});
if (!user) return null;

//? bcrypt๊ฐ€ DB์— ์ €์žฅ๋œ ๊ธฐ์กด password์™€ ๋„˜๊ฒจ ๋ฐ›์€ password๋ฅผ ๋Œ€์กฐํ•˜๊ณ ,
//? match false์‹œ 401์„ ๋ฆฌํ„ด
const isMatch = await bcrypt.compare(userSignInDto.password, user.password);
if (!isMatch) return sc.UNAUTHORIZED;

return user.id;
} catch (error) {
console.log(error);
throw error;
}
};

// const createUser = async (userCreateDTO: UserCreateDTO) => {
// const user = await prisma.user.create({
// data: {
// userName: userCreateDTO.name,
// age: userCreateDTO.age,
// email: userCreateDTO.email,
// },
// });
// return user;
// };

const createUser = async (userCreateDto: UserCreateDTO) => {
//? ๋„˜๊ฒจ๋ฐ›์€ password๋ฅผ bcrypt์˜ ๋„์›€์„ ๋ฐ›์•„ ์•”ํ˜ธํ™”
const salt = await bcrypt.genSalt(10); //^ ๋งค์šฐ ์ž‘์€ ์ž„์˜์˜ ๋žœ๋ค ํ…์ŠคํŠธ salt
const password = await bcrypt.hash(userCreateDto.password, salt); //^ ์œ„์—์„œ ๋žœ๋ค์„ ์ƒ์„ฑํ•œ salt๋ฅผ ์ด์šฉํ•ด ์•”ํ˜ธํ™”

const data = await prisma.user.create({
data: {
userName: userCreateDTO.name,
age: userCreateDTO.age,
email: userCreateDTO.email,
userName: userCreateDto?.name,
age: userCreateDto?.age,
email: userCreateDto.email,
password,
},
});
return user;

return data;
};

const getAllUser = async () => {
Expand Down Expand Up @@ -52,6 +93,7 @@ const userService = {
getAllUser,
updateUser,
deleteUser,
signIn,
};

export default userService;
Loading