import { OAuth2Client } from 'google-auth-library' import { authPool } from '../db/authDb.js' import { signToken, setTokenCookie } from './jwtProvider.js' import { getSetting } from '../settings/settingsService.js' import { AuthError, getUserInfo } from './authService.js' import type { Response } from 'express' const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '' const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID) interface GoogleProfile { sub: string email: string name: string picture?: string hd?: string // hosted domain (Google Workspace) } export async function googleLogin( credential: string, ipAddr: string, userAgent: string, res: Response ) { const profile = await verifyGoogleToken(credential) // 1. OAUTH_SUB로 기존 사용자 조회 let userResult = await authPool.query( 'SELECT USER_ID as user_id, USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE OAUTH_PROVIDER = $1 AND OAUTH_SUB = $2', ['GOOGLE', profile.sub] ) let userId: string if (userResult.rows.length > 0) { // 기존 OAuth 사용자 const user = userResult.rows[0] userId = user.user_id if (user.user_stts_cd === 'PENDING') { throw new AuthError('계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.', 403) } if (user.user_stts_cd === 'LOCKED') { throw new AuthError('계정이 잠겨있습니다. 관리자에게 문의하세요.', 403) } if (user.user_stts_cd === 'INACTIVE') { throw new AuthError('비활성화된 계정입니다.', 403) } if (user.user_stts_cd === 'REJECTED') { throw new AuthError('가입이 거절된 계정입니다. 관리자에게 문의하세요.', 403) } } else { // EMAIL로 기존 PW 사용자 조회 (계정 연결) userResult = await authPool.query( 'SELECT USER_ID as user_id, USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE EMAIL = $1 OR USER_ACNT = $1', [profile.email] ) if (userResult.rows.length > 0) { // 기존 계정에 OAuth 연결 const user = userResult.rows[0] userId = user.user_id await authPool.query( 'UPDATE AUTH_USER SET OAUTH_PROVIDER = $1, OAUTH_SUB = $2, EMAIL = $3, MDFCN_DTM = NOW() WHERE USER_ID = $4', ['GOOGLE', profile.sub, profile.email, userId] ) if (user.user_stts_cd !== 'ACTIVE') { throw new AuthError('계정이 활성 상태가 아닙니다. 관리자에게 문의하세요.', 403) } } else { // 신규 사용자 생성 userId = await createOAuthUser(profile) } } // 로그인 처리 await authPool.query( 'UPDATE AUTH_USER SET LAST_LOGIN_DTM = NOW(), MDFCN_DTM = NOW() WHERE USER_ID = $1', [userId] ) await recordLoginHistory(userId, ipAddr, userAgent) const userInfo = await getUserInfo(userId) // PENDING 사용자는 JWT 발급하지 않음 if (userInfo.roles.length === 0) { const userStatus = await authPool.query( 'SELECT USER_STTS_CD as user_stts_cd FROM AUTH_USER WHERE USER_ID = $1', [userId] ) if (userStatus.rows[0]?.user_stts_cd === 'PENDING') { throw new AuthError('계정이 승인 대기 중입니다. 관리자 승인 후 로그인할 수 있습니다.', 403) } } const token = signToken({ sub: userInfo.id, acnt: userInfo.account, name: userInfo.name, roles: userInfo.roles, }) setTokenCookie(res, token) return userInfo } async function verifyGoogleToken(credential: string): Promise { try { const ticket = await googleClient.verifyIdToken({ idToken: credential, audience: GOOGLE_CLIENT_ID, }) const payload = ticket.getPayload() if (!payload || !payload.email || !payload.sub) { throw new AuthError('Google 인증 정보가 유효하지 않습니다.', 401) } return { sub: payload.sub, email: payload.email, name: payload.name || payload.email.split('@')[0], picture: payload.picture, hd: payload.hd, } } catch (err) { if (err instanceof AuthError) throw err throw new AuthError('Google 인증 토큰 검증에 실패했습니다.', 401) } } async function createOAuthUser(profile: GoogleProfile): Promise { const domain = profile.email.split('@')[1] // 자동 승인 도메인 확인 const autoApproveDomains = await getSetting('oauth.auto-approve-domains') const allowedDomains = autoApproveDomains ? autoApproveDomains.split(',').map(d => d.trim().toLowerCase()) : [] const isAutoApproved = allowedDomains.includes(domain.toLowerCase()) const status = isAutoApproved ? 'ACTIVE' : 'PENDING' // 사용자 생성 const result = await authPool.query( `INSERT INTO AUTH_USER (USER_ACNT, USER_NM, EMAIL, OAUTH_PROVIDER, OAUTH_SUB, USER_STTS_CD) VALUES ($1, $2, $3, $4, $5, $6) RETURNING USER_ID as user_id`, [profile.email, profile.name, profile.email, 'GOOGLE', profile.sub, status] ) const userId = result.rows[0].user_id // 자동 승인된 사용자에게 기본 역할 할당 if (isAutoApproved) { await authPool.query( `INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) SELECT $1, ROLE_SN FROM AUTH_ROLE WHERE DFLT_YN = 'Y'`, [userId] ) } return userId } async function recordLoginHistory( userId: string, ipAddr: string, userAgent: string ): Promise { await authPool.query( `INSERT INTO AUTH_LOGIN_HIST (USER_ID, IP_ADDR, USER_AGENT, SUCCESS_YN) VALUES ($1, $2, $3, 'Y')`, [userId, ipAddr, userAgent?.substring(0, 500)] ) }