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

feat: login and logout #15

Open
wants to merge 2 commits into
base: develop
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
27 changes: 22 additions & 5 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,47 @@
"private": true,
"dependencies": {
"@typegoose/typegoose": "^12.9.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"express-asyncify": "^2.1.2",
"express-session": "^1.18.1",
"express-winston": "^4.2.0",
"helmet": "^8.0.0",
"hpp": "^0.2.3",
"mongoose": "^8.8.1",
"winston": "^3.16.0",
"passport": "^0.4.1",
"passport-apple": "^2.0.2",
"passport-google-oauth20": "^2.0.0",
"passport-kakao": "^1.0.1",
"passport-naver": "^1.0.6",
"reflect-metadata": "^0.2.2",
"routing-controllers": "^0.10.4",
"tslib": "^2.8.1",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@swc-node/register": "^1.10.9",
"@swc/cli": "^0.5.0",
"@swc/core": "^1.9.1",
"@swc/helpers": "^0.5.13",
"@swc/core": "^1.9.2",
"@swc/helpers": "^0.5.15",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/express-session": "1.17.0",
"@types/hpp": "^0.2.6",
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.7",
"@types/passport": "^1.0.17",
"@types/passport-kakao": "^1.0.3",
"express-asyncify": "^1.0.0",
"nodemon": "^3.1.7",
"swc-node": "^1.0.0"
"rimraf": "^6.0.1",
"swc-node": "^1.0.0",
"typescript": "^5"
}
}
36 changes: 35 additions & 1 deletion apps/server/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@ import { config } from 'dotenv'

config()

export const { NODE_ENV, DB_URI, DB_NAME } = process.env
export const {
NODE_ENV,
DB_URI,
DB_NAME,
JWT_SECRET,
KAKAO_CLIENT_ID,
KAKAO_CLIENT_SECRET,
KAKAO_CALLBACK_URI,
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
GOOGLE_CALLBACK_URI,
SESSION_SECRET,
} = process.env

export const PORT = Number.parseInt(process.env.PORT) || 5000

export default {
kakao: {
clientId: process.env.KAKAO_CLIENT_ID || '',
clientSecret: process.env.KAKAO_CLIENT_SECRET || '',
redirectUri: process.env.KAKAO_CALLBACK_URI || '',
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
redirectUri: process.env.GOOGLE_CALLBACK_URI || '',
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '1h',
},
session: {
secret: process.env.SESSION_SECRET || 'default_session_secret',
resave: false,
saveUninitialized: false,
},
}
61 changes: 61 additions & 0 deletions apps/server/src/config/passport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { PassportStatic } from 'passport'
import { Strategy as KakaoStrategy } from 'passport-kakao'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import config from '@/config'
import { findOrCreateUser } from '../services/v1/auth'
import { User } from '@/models/user'
import { GoogleProfile, KakaoProfile } from '@/types/oauth.profile'
import { findUserById } from '@/services/v1/user'

export default function configurePassport(passport: PassportStatic): void {
passport.serializeUser((user: User, done) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

세션을 로그인 과정에서 어떻게 이용하시나요?
oauth 로그인 이후에, jwt 를 기준으로 저희만의 jwt로 sign 해서 클라이언트에게 전달하는 로직으로 확인해서요..!

done(null, user._id)
})

passport.deserializeUser(async (id: string, done) => {
try {
const user = await findUserById(id)
done(null, user)
} catch (err) {
done(err, null)
}
})

passport.use(
'kakao',
new KakaoStrategy(
{
clientID: config.kakao.clientId,
clientSecret: config.kakao.clientSecret,
callbackURL: config.kakao.redirectUri,
},
async (_accessToken: string, _refreshToken: string, profile: KakaoProfile, done) => {
try {
const user: User = await findOrCreateUser('kakao', profile)
return done(null, user)
} catch (error) {
return done(error, null)
}
},
),
)

passport.use(
'google',
new GoogleStrategy(
{
clientID: config.google.clientId,
clientSecret: config.google.clientSecret,
callbackURL: config.google.redirectUri,
},
async (_accessToken: string, _refreshToken: string, profile: GoogleProfile, done) => {
try {
const user: User = await findOrCreateUser('google', profile)
return done(null, user)
} catch (error) {
return done(error, null)
}
},
),
)
}
48 changes: 48 additions & 0 deletions apps/server/src/controllers/v1/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AuthFailureException } from '@/exceptions/AuthFailureException'
import { LogoutFailedException } from '@/exceptions/LogoutFailedException '
import { User } from '@/models/user'
import { generateJWT } from '@/services/v1/auth'
import { Request, Response } from 'express'
import passport from 'passport'

export const authenticate = (provider: string) => {
return passport.authenticate(provider, { scope: getScopes(provider) })
}

export const callback = (provider: string) => {
return [
passport.authenticate(provider, { failureRedirect: '/auth/failure' }),
(req: Request, res: Response) => {
const user = req.user as User
const token = generateJWT(user)
res.json({ token })
},
]
}

export const authFailure = (req: Request, res: Response) => {
throw new AuthFailureException()
}

export const logout = (req: Request, res: Response) => {
req.logout()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도하신 로그아웃 기능이 사용자 정보를 저장한 세션을 만료시키는 것이 맞을까요?


req.session.destroy(err => {
if (err) {
throw new LogoutFailedException()
}
res.clearCookie('connect.sid')
res.status(200).send()
})
}

const getScopes = (provider: string): string[] => {
switch (provider) {
case 'google':
return ['profile', 'email']
case 'kakao':
return ['profile_nickname']
default:
return []
}
}
11 changes: 3 additions & 8 deletions apps/server/src/controllers/v1/enquiries.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import express, { Request, Response } from 'express'
import asyncify from 'express-asyncify'
import { Request, Response } from 'express'

import { EnquiryModel } from '@/models/enquiry'

const router = asyncify(express.Router())

// TODO: add verify user middleware
router.post('/', async (req: Request, res: Response) => {
export const createEnquiry = async (req: Request, res: Response) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 왜 수정하신걸까요?

const enquiry = await EnquiryModel.create({
userId: req.user._id,
title: req.body.title,
Expand All @@ -15,6 +12,4 @@ router.post('/', async (req: Request, res: Response) => {
res.status(200).json({
enquiryId: enquiry._id,
})
})

export default router
}
5 changes: 0 additions & 5 deletions apps/server/src/controllers/v1/index.ts

This file was deleted.

9 changes: 9 additions & 0 deletions apps/server/src/exceptions/AuthFailureException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpException } from '@/exceptions/HttpException'

export class AuthFailureException extends HttpException {
constructor() {
super(401, 'Authentication Failed')
Object.setPrototypeOf(this, AuthFailureException.prototype)
Error.captureStackTrace(this, AuthFailureException)
}
}
20 changes: 20 additions & 0 deletions apps/server/src/exceptions/HttpException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HttpError } from 'routing-controllers'

export class HttpException extends HttpError {
public status: number
public message: string

constructor(status: number, message: string) {
super(status, message)
this.status = status
this.message = message
}

toJSON() {
return {
status: this.status,
message: this.message,
stack: this.stack,
}
}
}
9 changes: 9 additions & 0 deletions apps/server/src/exceptions/LogoutFailedException .ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpException } from '@/exceptions/HttpException'

export class LogoutFailedException extends HttpException {
constructor() {
super(500, 'Failed to log out.')
Object.setPrototypeOf(this, LogoutFailedException.prototype)
Error.captureStackTrace(this, LogoutFailedException)
}
}
9 changes: 9 additions & 0 deletions apps/server/src/exceptions/NotFoundRouteException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpException } from '@/exceptions/HttpException'

export class NotFoundRouteException extends HttpException {
constructor() {
super(404, 'Route not found')
Object.setPrototypeOf(this, NotFoundRouteException.prototype)
Error.captureStackTrace(this, NotFoundRouteException)
}
}
9 changes: 9 additions & 0 deletions apps/server/src/middlewares/v1/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AuthFailureException } from '@/exceptions/AuthFailureException'
import { Request, Response, NextFunction } from 'express'

export function ensureAuthenticated(req: Request, res: Response, next: NextFunction) {
if (req.isAuthenticated()) {
return next()
}
throw new AuthFailureException()
}
12 changes: 6 additions & 6 deletions apps/server/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import mongoose from 'mongoose'
import { TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { getModelForClass, prop } from '@typegoose/typegoose'
import { TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'

export class User extends TimeStamps {
public _id: mongoose.Types.ObjectId

@prop({ required: true })
public nickname: string
public nickname!: string

@prop()
public provider: string
@prop({ required: true })
public provider!: string

@prop()
public providerId: string
@prop({ required: true })
public providerId!: string
}

export const UserModel = getModelForClass(User)
18 changes: 18 additions & 0 deletions apps/server/src/routers/v1/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { authenticate, authFailure, callback, logout } from '@/controllers/v1/auth'
import express from 'express'
import asyncify from 'express-asyncify'

const router = asyncify(express.Router())

const providers = ['kakao', 'google']

providers.forEach(provider => {
router.get(`/${provider}`, authenticate(provider))
router.get(`/${provider}/callback`, ...callback(provider))
})

router.get('/failure', authFailure)

router.post('/logout', logout)

export default router
10 changes: 10 additions & 0 deletions apps/server/src/routers/v1/enquiries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import express from 'express'
import asyncify from 'express-asyncify'
import { createEnquiry } from '@/controllers/v1/enquiries'

const router = asyncify(express.Router())

// TODO: add verify user middleware
router.post('/', createEnquiry)

export default router
15 changes: 15 additions & 0 deletions apps/server/src/routers/v1/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import express from 'express'
import asyncify from 'express-asyncify'
import { ensureAuthenticated } from '@/middlewares/v1/auth'
import { logout } from '@/controllers/v1/auth'

const router = asyncify(express.Router())

router.get('/logout', logout)

router.get('/profile', ensureAuthenticated, (req, res) => {
const user = req.user
res.json({ user })
})

export default router
Loading