-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement Google OAuth login with automatic user registration
- Loading branch information
Showing
38 changed files
with
606 additions
and
153 deletions.
There are no files selected for viewing
This file contains 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 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 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 |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import '@testing-library/jest-dom'; | ||
import "@testing-library/jest-dom"; | ||
|
||
// 전역 설정이나 모킹이 필요한 경우 여기에 추가 |
This file contains 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 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 @@ | ||
export * from "./supabase"; |
This file contains 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,27 @@ | ||
import { createClient } from "@supabase/supabase-js"; | ||
import dotenv from "dotenv"; | ||
import path from "path"; | ||
|
||
const env = process.env.NODE_ENV || "development"; | ||
const envPath = path.resolve(__dirname, `../../../../.env.${env}`); | ||
|
||
dotenv.config({ path: envPath }); | ||
|
||
if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { | ||
throw new Error("Missing env.NEXT_PUBLIC_SUPABASE_URL"); | ||
} | ||
|
||
if (!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { | ||
throw new Error("Missing env.NEXT_PUBLIC_SUPABASE_ANON_KEY"); | ||
} | ||
|
||
export const supabase = createClient( | ||
process.env.NEXT_PUBLIC_SUPABASE_URL, | ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, | ||
{ | ||
auth: { | ||
detectSessionInUrl: true, // OAuth 인증 정보를 URL에서 제거 | ||
flowType: "pkce", | ||
}, | ||
}, | ||
); |
This file contains 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,71 @@ | ||
import { AuthProvider } from "@shared/types/authTypes"; | ||
import { NextFunction, Request, Response } from "express"; | ||
import { AuthService } from "../services/auth.service"; | ||
|
||
export class AuthController { | ||
private authService: AuthService; | ||
|
||
constructor() { | ||
this.authService = new AuthService(); | ||
} | ||
|
||
async providerAuthStart(req: Request, res: Response, next: NextFunction) { | ||
try { | ||
const { provider } = req.query; | ||
if (!provider) { | ||
throw new Error("Missing authentication provider"); | ||
} | ||
|
||
const authUrl = await this.authService.getProviderAuthUrl( | ||
provider as AuthProvider, | ||
); | ||
return res.status(200).json({ url: authUrl }); | ||
} catch (error) { | ||
next(error) | ||
} | ||
} | ||
|
||
async providerAuth(req: Request, res: Response, next: NextFunction) { | ||
try { | ||
const code = req.query.code; | ||
const error = req.query.error; | ||
|
||
if (error || !code) { | ||
throw new Error(error?.toString() || "No auth code provided"); | ||
} | ||
|
||
const { userId, access_token, refresh_token } = await this.authService.getUserSessionData(code as string); | ||
const isNewUser = await this.authService.isNewUser(userId); | ||
|
||
if (isNewUser) { | ||
await this.authService.createUser({ id: userId }); | ||
} | ||
|
||
// 쿠키 설정 | ||
res.cookie("s-access-token", access_token, { | ||
maxAge: 60 * 1000, // 1분 | ||
secure: process.env.NODE_ENV === "production", | ||
}); | ||
|
||
res.cookie("sb-refresh-token", refresh_token, { | ||
httpOnly: true, | ||
secure: process.env.NODE_ENV === "production", | ||
sameSite: "lax", | ||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30일 | ||
}); | ||
|
||
/** | ||
* @info Supabase가 리다이렉트를 자동으로 수행하기 때문에 JSON으로 응답을 처리할 수 없습니다. | ||
*/ | ||
return res.redirect( | ||
`${process.env.NEXT_PUBLIC_WEB_URL}/auth/callback?isNewUser=${isNewUser}` | ||
); | ||
} catch (error) { | ||
return res.redirect( | ||
`${process.env.NEXT_PUBLIC_WEB_URL}/auth/callback?error=${encodeURIComponent( | ||
error instanceof Error ? error.message : 'An unknown error occurred' | ||
)}` | ||
); | ||
} | ||
} | ||
} |
This file contains 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 |
---|---|---|
@@ -1,5 +1,35 @@ | ||
import { logger } from '@shared/utils'; | ||
import cors from "cors"; | ||
import dotenv from "dotenv"; | ||
import express from "express"; | ||
import path from "path"; | ||
import { errorMiddleware } from "./middlewares/error.middleware"; | ||
import { authRoutes } from "./routes/auth.routes"; | ||
|
||
logger('Shared is connected', { | ||
state: 'ON', | ||
}); | ||
const env = process.env.NODE_ENV || "development"; | ||
const envPath = path.resolve(__dirname, `../../../../.env.${env}`); | ||
|
||
dotenv.config({ path: envPath }); | ||
|
||
const app = express(); | ||
const port = process.env.PORT || 4000; | ||
|
||
app.use( | ||
cors({ | ||
origin: process.env.NEXT_PUBLIC_WEB_URL, | ||
credentials: true, | ||
}), | ||
); | ||
|
||
app.use(express.json()); | ||
app.use("/api/auth", authRoutes); | ||
app.use(errorMiddleware); | ||
|
||
app.get("/api/health", (req, res) => { | ||
res.json({ status: "ok" }); | ||
}); | ||
|
||
app.listen(port, () => { | ||
console.log(`Server is running on port ${port}`); | ||
}); | ||
|
||
export { app }; |
This file contains 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,45 @@ | ||
import { NextFunction, Request, Response } from "express"; | ||
|
||
export interface ApiError { | ||
success: false; | ||
error: { | ||
code: string; // 애플리케이션 에러 코드 (예: AUTH_001, USER_002) | ||
message: string; // 사용자에게 보여줄 메시지 | ||
details?: string; // 개발자를 위한 상세 메시지 | ||
timestamp: string; // 에러 발생 시간 | ||
path: string; // 에러가 발생한 엔드포인트 | ||
traceId: string; // 요청 추적을 위한 ID | ||
stack?: string; // 개발 환경에서만 포함 | ||
}; | ||
} | ||
|
||
const generateId = () => { | ||
const timestamp = new Date().toISOString().replace(/[-:.]/g, ""); | ||
const random = Math.random().toString(36).substring(2, 8); | ||
return `${timestamp}-${random}`; | ||
}; | ||
|
||
export const errorMiddleware = ( | ||
error: any, | ||
req: Request, | ||
res: Response, | ||
next: NextFunction, | ||
) => { | ||
const isDev = process.env.NODE_ENV === "development"; | ||
const status = error.status || 500; | ||
|
||
const errorResponse: ApiError = { | ||
success: false, | ||
error: { | ||
code: error.code || `ERR_${status}`, | ||
message: error.message || "Internal Server Error", | ||
details: error.details, | ||
timestamp: new Date().toISOString(), | ||
path: req.path, | ||
traceId: generateId(), | ||
}, | ||
...(isDev && { stack: error.stack }), | ||
}; | ||
|
||
return res.status(status).json(errorResponse); | ||
}; |
This file contains 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,28 @@ | ||
import { supabase } from "../config/supabase"; | ||
|
||
interface Profile { | ||
/** | ||
* @todo type 정의 | ||
*/ | ||
} | ||
|
||
type SupabaseResponse<T> = { | ||
data: T | null; | ||
error: { message: string; code: string } | null; | ||
}; | ||
export class UserModel { | ||
async findByUserId(authUserId: string) { | ||
return supabase.from("profiles").select("*").eq("id", authUserId); | ||
} | ||
|
||
async createUser(id: string): Promise<SupabaseResponse<Profile>> { | ||
const now = new Date().toISOString(); | ||
return supabase.from("profiles").insert([ | ||
{ | ||
id, | ||
created_at: now, | ||
updated_at: now, | ||
}, | ||
]); | ||
} | ||
} |
This file contains 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,12 @@ | ||
import { Router } from "express"; | ||
import { AuthController } from "../controllers/auth.controller"; | ||
|
||
const router = Router(); | ||
const authController = new AuthController(); | ||
|
||
router.get("/provider", authController.providerAuthStart.bind(authController)); | ||
router.get( | ||
"/provider/callback", | ||
authController.providerAuth.bind(authController), | ||
); | ||
export { router as authRoutes }; |
Oops, something went wrong.