작업계획서대로 진행
This commit is contained in:
127
backend/api/auth/google/callback.get.ts
Normal file
127
backend/api/auth/google/callback.get.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { query, execute, insertReturning } from '../../../utils/db'
|
||||
import { getClientIp } from '../../../utils/ip'
|
||||
import { createSession, setSessionCookie } from '../../../utils/session'
|
||||
|
||||
/**
|
||||
* Google OAuth 콜백
|
||||
* GET /api/auth/google/callback
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const params = getQuery(event)
|
||||
const clientIp = getClientIp(event)
|
||||
const userAgent = getHeader(event, 'user-agent') || null
|
||||
|
||||
const code = params.code as string
|
||||
const state = params.state as string
|
||||
const error = params.error as string
|
||||
|
||||
if (error) {
|
||||
return sendRedirect(event, '/login?error=oauth_denied')
|
||||
}
|
||||
|
||||
// State 검증
|
||||
const savedState = getCookie(event, 'oauth_state')
|
||||
if (!savedState || savedState !== state) {
|
||||
return sendRedirect(event, '/login?error=invalid_state')
|
||||
}
|
||||
deleteCookie(event, 'oauth_state')
|
||||
|
||||
const clientId = config.googleClientId || process.env.GOOGLE_CLIENT_ID
|
||||
const clientSecret = config.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET
|
||||
const redirectUri = config.googleRedirectUri || process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/api/auth/google/callback'
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
return sendRedirect(event, '/login?error=oauth_not_configured')
|
||||
}
|
||||
|
||||
try {
|
||||
// 토큰 교환
|
||||
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code'
|
||||
})
|
||||
})
|
||||
|
||||
const tokenData = await tokenRes.json()
|
||||
if (!tokenData.access_token) {
|
||||
console.error('Token error:', tokenData)
|
||||
return sendRedirect(event, '/login?error=token_failed')
|
||||
}
|
||||
|
||||
// 사용자 정보 조회
|
||||
const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
||||
})
|
||||
const googleUser = await userRes.json()
|
||||
|
||||
if (!googleUser.email) {
|
||||
return sendRedirect(event, '/login?error=no_email')
|
||||
}
|
||||
|
||||
const googleId = googleUser.id
|
||||
const googleEmail = googleUser.email.toLowerCase()
|
||||
const googleName = googleUser.name || googleEmail.split('@')[0]
|
||||
|
||||
// 기존 사용자 조회 (이메일 기준)
|
||||
let employees = await query<any>(`
|
||||
SELECT * FROM wr_employee_info WHERE employee_email = $1
|
||||
`, [googleEmail])
|
||||
|
||||
let employee = employees[0]
|
||||
|
||||
if (employee) {
|
||||
// 기존 사용자 - Google 정보 연결
|
||||
await execute(`
|
||||
UPDATE wr_employee_info SET
|
||||
google_id = $1,
|
||||
google_email = $2,
|
||||
google_linked_at = NOW(),
|
||||
google_access_token = $3,
|
||||
google_refresh_token = $4,
|
||||
google_token_expires_at = NOW() + INTERVAL '${tokenData.expires_in} seconds',
|
||||
last_login_at = NOW(),
|
||||
last_login_ip = $5,
|
||||
updated_at = NOW()
|
||||
WHERE employee_id = $6
|
||||
`, [googleId, googleEmail, tokenData.access_token, tokenData.refresh_token || null, clientIp, employee.employee_id])
|
||||
} else {
|
||||
// 신규 사용자 자동 등록
|
||||
employee = await insertReturning(`
|
||||
INSERT INTO wr_employee_info (
|
||||
employee_name, employee_email, google_id, google_email, google_linked_at,
|
||||
google_access_token, google_refresh_token, google_token_expires_at,
|
||||
last_login_at, last_login_ip, created_ip, created_email
|
||||
) VALUES ($1, $2, $3, $2, NOW(), $4, $5, NOW() + INTERVAL '${tokenData.expires_in} seconds', NOW(), $6, $6, $2)
|
||||
RETURNING *
|
||||
`, [googleName, googleEmail, googleId, tokenData.access_token, tokenData.refresh_token || null, clientIp])
|
||||
}
|
||||
|
||||
// 로그인 이력 추가
|
||||
const loginHistory = await insertReturning(`
|
||||
INSERT INTO wr_login_history (employee_id, login_ip, login_email, login_type)
|
||||
VALUES ($1, $2, $3, 'GOOGLE')
|
||||
RETURNING history_id
|
||||
`, [employee.employee_id, clientIp, googleEmail])
|
||||
|
||||
// 세션 생성
|
||||
const sessionId = await createSession(
|
||||
employee.employee_id,
|
||||
loginHistory.history_id,
|
||||
clientIp,
|
||||
userAgent
|
||||
)
|
||||
setSessionCookie(event, sessionId)
|
||||
|
||||
return sendRedirect(event, '/')
|
||||
} catch (e) {
|
||||
console.error('Google OAuth error:', e)
|
||||
return sendRedirect(event, '/login?error=oauth_failed')
|
||||
}
|
||||
})
|
||||
35
backend/api/auth/google/index.get.ts
Normal file
35
backend/api/auth/google/index.get.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Google OAuth 시작
|
||||
* GET /api/auth/google
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const clientId = config.googleClientId || process.env.GOOGLE_CLIENT_ID
|
||||
const redirectUri = config.googleRedirectUri || process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/api/auth/google/callback'
|
||||
|
||||
if (!clientId) {
|
||||
throw createError({ statusCode: 500, message: 'Google OAuth가 설정되지 않았습니다.' })
|
||||
}
|
||||
|
||||
const scope = encodeURIComponent('openid email profile')
|
||||
const state = Math.random().toString(36).substring(7) // CSRF 방지
|
||||
|
||||
// state를 쿠키에 저장
|
||||
setCookie(event, 'oauth_state', state, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 300 // 5분
|
||||
})
|
||||
|
||||
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
|
||||
`client_id=${clientId}` +
|
||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
||||
`&response_type=code` +
|
||||
`&scope=${scope}` +
|
||||
`&state=${state}` +
|
||||
`&access_type=offline` +
|
||||
`&prompt=consent`
|
||||
|
||||
return sendRedirect(event, authUrl)
|
||||
})
|
||||
Reference in New Issue
Block a user