Skip to content
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
343 changes: 339 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
"dotenv": "^17.4.2",
"express": "^5.2.1",
"http-status-codes": "^2.3.0",
"jsonwebtoken": "^9.0.3",
"morgan": "^1.10.1",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"swagger-autogen": "^2.23.7",
"swagger-ui-express": "^5.0.1",
"tsoa": "^7.0.0-alpha.0"
Expand All @@ -34,8 +40,14 @@
"@types/cors": "^2.8.19",
"@types/dotenv": "^6.1.1",
"@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.10",
"@types/node": "^25.6.2",
"@types/passport": "^1.0.17",
"@types/passport-github2": "^1.2.9",
"@types/passport-google-oauth20": "^2.0.17",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/swagger-ui-express": "^4.1.8",
"nodemon": "^3.1.14",
"prisma": "^7.8.0",
Expand Down
8 changes: 5 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ model User {
userId Int @id @default(autoincrement())
email String @unique
password String
phoneNumber String
phoneNumber String?

provider String

role String @default("USER")
name String
gender String
birth DateTime
gender String?
birth DateTime?

address String?
city String?
Expand Down
281 changes: 281 additions & 0 deletions src/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import dotenv from "dotenv";
import {
Strategy as GoogleStrategy,
Profile,
} from "passport-google-oauth20";

import { Strategy as GitHubStrategy } from "passport-github2";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
import { Strategy as LocalStrategy } from "passport-local";
import bcrypt from "bcrypt";

import jwt from "jsonwebtoken";
import { prisma } from "./db.config.js"; // Prisma 설정 파일 경로 확인 필요
import { CustomError } from "./common/errors/custom.error.js";

dotenv.config();

// 1. JWT 토큰 생성 함수 (타입 지정)
export const generateAccessToken = (user: {
userId: number;
email: string;
}) => {
return jwt.sign(
{
userId: user.userId,
email: user.email,
},
process.env.JWT_SECRET!,
{ expiresIn: "1h" }
);
};

export const generateRefreshToken = (user: {
userId: number;
}) => {
return jwt.sign(
{
userId: user.userId,
},
process.env.JWT_SECRET!,
{ expiresIn: "14d" }
);
};

// 2. Google Verify 로직
const googleVerify = async (profile: Profile) => {
const email = profile.emails?.[0]?.value;

if (!email) {
throw new CustomError(
400,
"Google 프로필에 이메일이 없습니다.",
"GOOGLE_EMAIL_REQUIRED",
);
}

let user = await prisma.user.findFirst({ where: { email } });

if (
user &&
user.provider !== "google"
) {

throw new CustomError(
403,
`${user.provider} 로그인만 가능합니다.`,
"INVALID_LOGIN_PROVIDER",
);
}

if (!user) {
user = await prisma.user.create({
data: {
provider: "google",
email,
password: "GOOGLE_LOGIN_USER",
name: profile.displayName,

gender: "추후 수정",
birth: new Date("1970-01-01"),

phoneNumber: "추후 수정",

address: "추후 수정",
city: "추후 수정",
district: "추후 수정",
neighborhood: "추후 수정",
detail: "추후 수정",
},
});
}

return {
userId: user.userId,
email: user.email,
name: user.name,
role: user.role,
};
};

// 3. Google Strategy
export const googleStrategy = new GoogleStrategy(
{
clientID: process.env.PASSPORT_GOOGLE_CLIENT_ID!,
clientSecret: process.env.PASSPORT_GOOGLE_CLIENT_SECRET!,
callbackURL: "auth/google/callback",
scope: ["email", "profile"],
},
async (_accessToken, _refreshToken, profile, cb) => {
try {
const user = await googleVerify(profile);
const tokens = {
accessToken: generateAccessToken(user),
refreshToken: generateRefreshToken(user),
};
return cb(null, tokens);
} catch (err) {
return cb(err as Error);
}
}
);

// Github Verify 로직
const githubVerify = async (profile: any) => {
const email =
profile.emails?.[0]?.value ||
`${profile.username}@github.com`;

let user = await prisma.user.findFirst({where: { email }});

if (
user &&
user.provider !== "github"
) {

throw new CustomError(
403,
`${user.provider} 로그인만 가능합니다.`,
"INVALID_LOGIN_PROVIDER",
);
}

if (!user) {
user = await prisma.user.create({
data: {
provider: "github",
email,
password: "GITHUB_LOGIN_USER",
name: profile.displayName || profile.username,

gender: "추후 수정",
birth: new Date("1970-01-01"),
phoneNumber: "추후 수정",

address: "추후 수정",
city: "추후 수정",
district: "추후 수정",
neighborhood: "추후 수정",
detail: "추후 수정",
},
});
}

return {
userId: user.userId,
email: user.email,
name: user.name,
role: user.role,
};
};

// Github Strategy
export const githubStrategy = new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: "auth/github/callback",
scope: ["user:email"],
},
async (_accessToken: string, _refreshToken: string, profile: any, cb: any) => {
try {
const user = await githubVerify(profile);

const tokens = {
accessToken: generateAccessToken(user),
refreshToken: generateRefreshToken(user),
};

return cb(null, tokens);
} catch (err) {
return cb(err as Error);
}
},
);

// JWT 검증 미들웨어
export const jwtStrategy = new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET!,
},
async (payload, done) => {
try {
const user = await prisma.user.findFirst({ where: { userId: payload.id } });
return user ? done(null, user) : done(null, false);
} catch (err) {
return done(err, false);
}
}
);

// Local Verify 로직
const localVerify = async (
email: string,
password: string
) => {
const user = await prisma.user.findFirst({
where: { email },
});

if (!user) {
throw new CustomError(
404,
"존재하지 않는 유저입니다.",
"USER_NOT_FOUND",
);
}

if (user.provider !== "local") {
throw new CustomError(
403,
`${user.provider} 로그인만 가능합니다.`,
"INVALID_LOGIN_PROVIDER",
);
}

const isMatch = await bcrypt.compare(
password,
user.password
);

if (!isMatch) {
throw new CustomError(
401,
"비밀번호가 일치하지 않습니다.",
"INVALID_PASSWORD",
);
}

return {
userId: user.userId,
email: user.email,
name: user.name,
role: user.role,
};
};

// local strategy
export const localStrategy = new LocalStrategy(
{
usernameField: "email",
passwordField: "password",
},
async (email, password, done) => {
try {
const user = await localVerify(
email,
password
);

const tokens = {
accessToken: generateAccessToken(user),
refreshToken: generateRefreshToken(user),
};

return done(null, tokens);
} catch (err) {
return done(err as Error);
}
},
);
Loading