-
Notifications
You must be signed in to change notification settings - Fork 3
[9주차/해피잭] 워크북 제출합니다 #21
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
Open
rbdus0715
wants to merge
5
commits into
UMC-Inha:happyjack/main
Choose a base branch
from
rbdus0715:feature/chapter-09
base: happyjack/main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
58762e9
feat: profile update api
rbdus0715 28da2e8
feat: add package for login
rbdus0715 220d727
feat: add oauth login
rbdus0715 0b409de
feat: naver oauth login and local login with password
rbdus0715 ef6a3c9
feat: can use only one auth
rbdus0715 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| import dotenv from 'dotenv'; | ||
| import { Strategy as GoogleStrategy, Profile } from 'passport-google-oauth20'; | ||
| import { | ||
| Strategy as NaverStrategy, | ||
| Profile as NaverProfile, | ||
| } from 'passport-naver-v2'; | ||
| import { Strategy as LocalStrategy } from 'passport-local'; | ||
| import bcrypt from 'bcrypt'; | ||
| import jwt from 'jsonwebtoken'; | ||
| import { prisma } from './config/db.config'; | ||
| import { user_gender, user_social_type } from './generated/prisma/enums.js'; | ||
| import { AppError } from './common/app-error'; | ||
| import { USER_ERROR_CODE } from './common/error-code'; | ||
| import { StatusCodes } from 'http-status-codes'; | ||
|
|
||
| dotenv.config(); | ||
|
|
||
| export type AuthUser = { | ||
| id: bigint; | ||
| email: string; | ||
| name: string; | ||
| }; | ||
|
|
||
| export type AuthTokens = { | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| }; | ||
|
|
||
| export const generateAccessToken = (user: { id: bigint; email: string }) => { | ||
| return jwt.sign( | ||
| { id: user.id.toString(), email: user.email }, | ||
| process.env.JWT_SECRET!, | ||
| { expiresIn: '1h' }, | ||
| ); | ||
| }; | ||
|
|
||
| export const generateRefreshToken = (user: { id: bigint }) => { | ||
| return jwt.sign({ id: user.id.toString() }, process.env.JWT_SECRET!, { | ||
| expiresIn: '14d', | ||
| }); | ||
| }; | ||
|
|
||
| const socialTypeLabel = (type: user_social_type | null): string => { | ||
| switch (type) { | ||
| case user_social_type.GOOGLE: | ||
| return 'Google'; | ||
| case user_social_type.NAVER: | ||
| return '네이버'; | ||
| // case user_social_type.KAKAO: | ||
| // return '카카오'; | ||
| // case user_social_type.APPLE: | ||
| // return 'Apple'; | ||
| case user_social_type.LOCAL: | ||
| return '이메일/비밀번호'; | ||
| default: | ||
| return 'default'; | ||
| } | ||
| }; | ||
|
|
||
| const assertSignUpMethod = ( | ||
| registered: user_social_type | null, | ||
| attempted: user_social_type, | ||
| ): void => { | ||
| if (registered !== attempted) { | ||
| throw new AppError( | ||
| USER_ERROR_CODE.SIGNUP_METHOD_MISMATCH, | ||
| `이미 ${socialTypeLabel(registered)} 방식으로 가입된 이메일입니다. ${socialTypeLabel(registered)} 로그인을 이용해주세요.`, | ||
| StatusCodes.CONFLICT, | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| const googleVerify = async (profile: Profile): Promise<AuthUser> => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 구글로그인 인증과 동시에 사용자 회원가입을 진행하는것 보다 /google에서 로그인을 진행하고 만약 해당 사용자가 회원가입 되지 않은 사용자라면 json 응답에 응답 종류와 ticket으로 /google/register 라우터로 리다이렉트 시키는방식으로 구현하는게 좋을것 같습니다 |
||
| const email = profile.emails?.[0]?.value; | ||
| if (!email) throw new Error('Google 프로필에 이메일이 없습니다.'); | ||
|
|
||
| let user = await prisma.user.findFirst({ where: { email } }); | ||
|
|
||
| if (!user) { | ||
| const nickname = (profile.displayName ?? email.split('@')[0] ?? 'user') | ||
| .replace(/\s+/g, '') | ||
| .slice(0, 10); | ||
|
|
||
| user = await prisma.user.create({ | ||
| data: { | ||
| email, | ||
| nickname, | ||
| gender: user_gender.NONE, | ||
| birth: new Date(1970, 0, 1), | ||
| social_id: profile.id, | ||
| social_type: user_social_type.GOOGLE, | ||
| created_at: new Date(), | ||
| }, | ||
| }); | ||
| } else { | ||
| assertSignUpMethod(user.social_type, user_social_type.GOOGLE); | ||
| } | ||
|
|
||
| return { id: user.id, email: user.email, name: user.nickname }; | ||
| }; | ||
|
|
||
| export const googleStrategy = new GoogleStrategy( | ||
| { | ||
| clientID: process.env.PASSPORT_GOOGLE_CLIENT_ID!, | ||
| clientSecret: process.env.PASSPORT_GOOGLE_CLIENT_SECRET!, | ||
| callbackURL: '/oauth2/callback/google', | ||
| scope: ['email', 'profile'], | ||
| }, | ||
| async (_accessToken, _refreshToken, profile, cb) => { | ||
| try { | ||
| const user = await googleVerify(profile); | ||
| const tokens: AuthTokens = { | ||
| accessToken: generateAccessToken(user), | ||
| refreshToken: generateRefreshToken(user), | ||
| }; | ||
| return cb(null, tokens); | ||
| } catch (err) { | ||
| return cb(err as Error); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| const naverVerify = async (profile: NaverProfile): Promise<AuthUser> => { | ||
| const email = profile.email; | ||
| if (!email) throw new Error('네이버 프로필에 이메일이 없습니다.'); | ||
|
|
||
| let user = await prisma.user.findFirst({ where: { email } }); | ||
|
|
||
| if (!user) { | ||
| const nickname = (profile.nickname ?? profile.name ?? email.split('@')[0] ?? 'user') | ||
| .replace(/\s+/g, '') | ||
| .slice(0, 10); | ||
|
|
||
| const gender = | ||
| profile.gender === 'M' | ||
| ? user_gender.MALE | ||
| : profile.gender === 'F' | ||
| ? user_gender.FEMALE | ||
| : user_gender.NONE; | ||
|
|
||
| user = await prisma.user.create({ | ||
| data: { | ||
| email, | ||
| nickname, | ||
| gender, | ||
| birth: new Date(1970, 0, 1), | ||
| social_id: profile.id, | ||
| social_type: user_social_type.NAVER, | ||
| created_at: new Date(), | ||
| }, | ||
| }); | ||
| } else { | ||
| assertSignUpMethod(user.social_type, user_social_type.NAVER); | ||
| } | ||
|
|
||
| return { id: user.id, email: user.email, name: user.nickname }; | ||
| }; | ||
|
|
||
| export const naverStrategy = new NaverStrategy( | ||
| { | ||
| clientID: process.env.PASSPORT_NAVER_CLIENT_ID!, | ||
| clientSecret: process.env.PASSPORT_NAVER_CLIENT_SECRET!, | ||
| callbackURL: '/oauth2/callback/naver', | ||
| }, | ||
| async ( | ||
| accessToken: string, | ||
| refreshToken: string, | ||
| profile: NaverProfile, | ||
| cb: (err: Error | null, user?: AuthTokens) => void, | ||
| ) => { | ||
| try { | ||
| const user = await naverVerify(profile); | ||
| const tokens: AuthTokens = { | ||
| accessToken: generateAccessToken(user), | ||
| refreshToken: generateRefreshToken(user), | ||
| }; | ||
| return cb(null, tokens); | ||
| } catch (err) { | ||
| return cb(err as Error); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| const BCRYPT_SALT_ROUNDS = 10; | ||
|
|
||
| export const hashPassword = (plain: string): Promise<string> => | ||
| bcrypt.hash(plain, BCRYPT_SALT_ROUNDS); | ||
|
|
||
| export type LocalSignUpInput = { | ||
| email: string; | ||
| password: string; | ||
| name?: string; | ||
| }; | ||
|
|
||
| export const localSignUp = async ( | ||
| input: LocalSignUpInput, | ||
| ): Promise<AuthUser> => { | ||
| const existing = await prisma.user.findFirst({ | ||
| where: { email: input.email }, | ||
| }); | ||
| if (existing) { | ||
| throw new AppError( | ||
| USER_ERROR_CODE.EMAIL_ALREADY_EXISTS, | ||
| '이미 존재하는 이메일입니다.', | ||
| StatusCodes.CONFLICT, | ||
| ); | ||
| } | ||
|
|
||
| const hashed = await hashPassword(input.password); | ||
| const nickname = (input.name ?? input.email.split('@')[0] ?? 'user') | ||
| .replace(/\s+/g, '') | ||
| .slice(0, 10); | ||
|
|
||
| const user = await prisma.user.create({ | ||
| data: { | ||
| email: input.email, | ||
| password: hashed, | ||
| nickname, | ||
| gender: user_gender.NONE, | ||
| birth: new Date(1970, 0, 1), | ||
| social_id: `local:${input.email}`, | ||
| social_type: user_social_type.LOCAL, | ||
| created_at: new Date(), | ||
| }, | ||
| }); | ||
|
|
||
| return { id: user.id, email: user.email, name: user.nickname }; | ||
| }; | ||
|
|
||
| const localVerify = async ( | ||
| email: string, | ||
| password: string, | ||
| ): Promise<AuthUser> => { | ||
| const user = await prisma.user.findFirst({ where: { email } }); | ||
|
|
||
| if (!user) { | ||
| throw new AppError( | ||
| USER_ERROR_CODE.INVALID_CREDENTIALS, | ||
| '이메일 또는 비밀번호가 올바르지 않습니다.', | ||
| StatusCodes.UNAUTHORIZED, | ||
| ); | ||
| } | ||
|
|
||
| if (user.social_type !== user_social_type.LOCAL || !user.password) { | ||
| assertSignUpMethod(user.social_type, user_social_type.LOCAL); | ||
| throw new AppError( | ||
| USER_ERROR_CODE.INVALID_CREDENTIALS, | ||
| '이메일 또는 비밀번호가 올바르지 않습니다.', | ||
| StatusCodes.UNAUTHORIZED, | ||
| ); | ||
| } | ||
|
|
||
| const matched = await bcrypt.compare(password, user.password); | ||
| if (!matched) { | ||
| throw new AppError( | ||
| USER_ERROR_CODE.INVALID_CREDENTIALS, | ||
| '이메일 또는 비밀번호가 올바르지 않습니다.', | ||
| StatusCodes.UNAUTHORIZED, | ||
| ); | ||
| } | ||
|
|
||
| return { id: user.id, email: user.email, name: user.nickname }; | ||
| }; | ||
|
|
||
| export const localStrategy = new LocalStrategy( | ||
| { | ||
| usernameField: 'email', | ||
| passwordField: 'password', | ||
| session: false, | ||
| }, | ||
| async (email, password, cb) => { | ||
| try { | ||
| const user = await localVerify(email, password); | ||
| const tokens: AuthTokens = { | ||
| accessToken: generateAccessToken(user), | ||
| refreshToken: generateRefreshToken(user), | ||
| }; | ||
| return cb(null, tokens); | ||
| } catch (err) { | ||
| if (err instanceof AppError) { | ||
| return cb(null, false, { message: err.message }); | ||
| } | ||
| return cb(err as Error); | ||
| } | ||
| }, | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
환경변수를 !으로 단언하기 보다 변수로 선언해 존재하는지를 예외처리하는것이 좋아보입니다