From fad322c218475d87fdc2a7cc87baaee0301336c9 Mon Sep 17 00:00:00 2001 From: Hyoseong Jo Date: Sun, 11 Jan 2026 23:42:50 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20Synology=20SSO=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20API=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Synology 버튼: '오솔NAS로 로그인'으로 변경 - 이메일 매칭: {username}@osolit.net으로 매칭 - 삭제된 API: - login.post.ts (이메일+이름 로그인) - recent-users.get.ts (최근 로그인 사용자) - select-user.post.ts (사용자 선택 로그인) --- frontend/auth/synology/callback.vue | 12 +- frontend/login.vue | 4 +- server/api/auth/login.post.ts | 84 ------------ server/api/auth/recent-users.get.ts | 23 ---- server/api/auth/select-user.post.ts | 58 -------- server/api/auth/synology/verify.post.ts | 168 ++++++++++++++++++------ 6 files changed, 136 insertions(+), 213 deletions(-) delete mode 100644 server/api/auth/login.post.ts delete mode 100644 server/api/auth/recent-users.get.ts delete mode 100644 server/api/auth/select-user.post.ts diff --git a/frontend/auth/synology/callback.vue b/frontend/auth/synology/callback.vue index 5c3c659..1046f1b 100644 --- a/frontend/auth/synology/callback.vue +++ b/frontend/auth/synology/callback.vue @@ -17,7 +17,6 @@ onMounted(async () => { const hash = window.location.hash.substring(1) const params = new URLSearchParams(hash) const accessToken = params.get('access_token') - const state = params.get('state') const error = params.get('error') if (error) { @@ -32,11 +31,16 @@ onMounted(async () => { try { // 서버로 access_token 전송하여 로그인 처리 - await $fetch('/api/auth/synology/verify', { + const result = await $fetch<{ success: boolean; needPasswordSet?: boolean }>('/api/auth/synology/verify', { method: 'POST', - body: { accessToken, state } + body: { accessToken } }) - router.push('/') + + if (result.needPasswordSet) { + router.push('/set-password?from=synology') + } else { + router.push('/') + } } catch (e: any) { router.push(`/login?error=${encodeURIComponent(e.data?.message || 'Synology 로그인에 실패했습니다.')}`) } diff --git a/frontend/login.vue b/frontend/login.vue index 0f4add7..cf7de9c 100644 --- a/frontend/login.vue +++ b/frontend/login.vue @@ -36,9 +36,9 @@ Google로 로그인 - + - Synology로 로그인 + 오솔NAS로 로그인 diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts deleted file mode 100644 index 60f8e0d..0000000 --- a/server/api/auth/login.post.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { getClientIp } from '../../utils/ip' -import { createSession, setSessionCookie } from '../../utils/session' - -interface LoginBody { - email: string - name: string -} - -/** - * 이메일+이름 로그인 - * POST /api/auth/login - */ -export default defineEventHandler(async (event) => { - const body = await readBody(event) - const clientIp = getClientIp(event) - const userAgent = getHeader(event, 'user-agent') || null - - if (!body.email || !body.name) { - throw createError({ statusCode: 400, message: '이메일과 이름을 입력해주세요.' }) - } - - // 이메일 형식 검증 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(body.email)) { - throw createError({ statusCode: 400, message: '올바른 이메일 형식이 아닙니다.' }) - } - - const emailLower = body.email.toLowerCase() - const nameTrimmed = body.name.trim() - - // 기존 직원 조회 - let employee = await query(` - SELECT * FROM wr_employee_info WHERE employee_email = $1 - `, [emailLower]) - - let employeeData = employee[0] - - if (employeeData) { - // 기존 직원 - 이름이 다르면 업데이트 - if (employeeData.employee_name !== nameTrimmed) { - await execute(` - UPDATE wr_employee_info - SET employee_name = $1, updated_at = NOW(), updated_ip = $2, updated_email = $3 - WHERE employee_id = $4 - `, [nameTrimmed, clientIp, emailLower, employeeData.employee_id]) - employeeData.employee_name = nameTrimmed - } - } else { - // 신규 직원 자동 등록 - employeeData = await insertReturning(` - INSERT INTO wr_employee_info (employee_name, employee_email, created_ip, created_email, updated_ip, updated_email) - VALUES ($1, $2, $3, $2, $3, $2) - RETURNING * - `, [nameTrimmed, emailLower, clientIp]) - } - - // 로그인 이력 추가 - const loginHistory = await insertReturning(` - INSERT INTO wr_login_history (employee_id, login_ip, login_email) - VALUES ($1, $2, $3) - RETURNING history_id - `, [employeeData.employee_id, clientIp, emailLower]) - - // DB 기반 세션 생성 - const sessionId = await createSession( - employeeData.employee_id, - loginHistory.history_id, - clientIp, - userAgent - ) - - // 세션 쿠키 설정 - setSessionCookie(event, sessionId) - - return { - success: true, - user: { - employeeId: employeeData.employee_id, - employeeName: employeeData.employee_name, - employeeEmail: employeeData.employee_email, - employeePosition: employeeData.employee_position - } - } -}) diff --git a/server/api/auth/recent-users.get.ts b/server/api/auth/recent-users.get.ts deleted file mode 100644 index 83900fb..0000000 --- a/server/api/auth/recent-users.get.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { query } from '../../utils/db' - -/** - * 최근 로그인 사용자 목록 - * GET /api/auth/recent-users - */ -export default defineEventHandler(async () => { - const users = await query(` - SELECT * FROM wr_recent_login_users - ORDER BY last_active_at DESC - LIMIT 10 - `) - - return { - users: users.map((u: any) => ({ - employeeId: u.employee_id, - employeeName: u.employee_name, - employeeEmail: u.employee_email, - employeePosition: u.employee_position, - lastActiveAt: u.last_active_at - })) - } -}) diff --git a/server/api/auth/select-user.post.ts b/server/api/auth/select-user.post.ts deleted file mode 100644 index d564073..0000000 --- a/server/api/auth/select-user.post.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { getClientIp } from '../../utils/ip' -import { createSession, setSessionCookie } from '../../utils/session' - -interface SelectUserBody { - employeeId: number -} - -/** - * 기존 사용자 선택 로그인 - * POST /api/auth/select-user - */ -export default defineEventHandler(async (event) => { - const body = await readBody(event) - const clientIp = getClientIp(event) - const userAgent = getHeader(event, 'user-agent') || null - - if (!body.employeeId) { - throw createError({ statusCode: 400, message: '사용자를 선택해주세요.' }) - } - - // 사원 조회 - const employee = await queryOne(` - SELECT * FROM wr_employee_info - WHERE employee_id = $1 AND is_active = true - `, [body.employeeId]) - - if (!employee) { - throw createError({ statusCode: 404, message: '사용자를 찾을 수 없습니다.' }) - } - - // 로그인 이력 추가 - const loginHistory = await insertReturning(` - INSERT INTO wr_login_history (employee_id, login_ip, login_email) - VALUES ($1, $2, $3) - RETURNING history_id - `, [employee.employee_id, clientIp, employee.employee_email]) - - // DB 기반 세션 생성 - const sessionId = await createSession( - employee.employee_id, - loginHistory.history_id, - clientIp, - userAgent - ) - - // 세션 쿠키 설정 - setSessionCookie(event, sessionId) - - return { - success: true, - user: { - employeeId: employee.employee_id, - employeeName: employee.employee_name, - employeeEmail: employee.employee_email, - employeePosition: employee.employee_position - } - } -}) diff --git a/server/api/auth/synology/verify.post.ts b/server/api/auth/synology/verify.post.ts index de9b7c3..ae23e50 100644 --- a/server/api/auth/synology/verify.post.ts +++ b/server/api/auth/synology/verify.post.ts @@ -2,11 +2,14 @@ * Synology SSO Access Token 검증 및 로그인 처리 * POST /api/auth/synology/verify */ -import { query } from '~/server/utils/db' +import { queryOne, execute } from '~/server/utils/db' +import { createSession } from '~/server/utils/session' +import { getClientIp } from '~/server/utils/ip' export default defineEventHandler(async (event) => { const body = await readBody(event) const { accessToken } = body + const ip = getClientIp(event) if (!accessToken) { throw createError({ @@ -17,71 +20,152 @@ export default defineEventHandler(async (event) => { const config = useRuntimeConfig() + console.log('[Synology SSO] Access Token:', accessToken) + try { - // Synology SSO Server에서 사용자 정보 조회 - const userInfoUrl = `${config.synologyServerUrl}/webman/sso/SSOAccessToken.cgi?action=exchange&access_token=${accessToken}&app_id=${config.synologyClientId}` + // 1. 액세스 토큰으로 사용자 정보 조회 + const userInfoUrl = `${config.synologyServerUrl}/webman/sso/SSOUserInfo.cgi` - const userInfoResponse = await $fetch(userInfoUrl) + console.log('[Synology SSO] Requesting user info...') - if (!userInfoResponse.success) { - throw createError({ - statusCode: 401, - message: userInfoResponse.error?.msg || 'Synology 토큰 검증에 실패했습니다.' - }) + let userResponse = await $fetch(userInfoUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }) + + // 문자열로 온 경우 JSON 파싱 + if (typeof userResponse === 'string') { + userResponse = JSON.parse(userResponse) } - const synologyUsername = userInfoResponse.data?.user_name || userInfoResponse.data?.user_id + console.log('[Synology SSO] User Response:', JSON.stringify(userResponse, null, 2)) - if (!synologyUsername) { - throw createError({ - statusCode: 401, - message: 'Synology 사용자 정보를 가져올 수 없습니다.' - }) + if (!userResponse.data || !userResponse.data.email) { + // email이 없으면 SSOAccessToken.cgi로 시도 + const tokenInfoUrl = `${config.synologyServerUrl}/webman/sso/SSOAccessToken.cgi?action=exchange&access_token=${accessToken}&app_id=${config.synologyClientId}` + + let tokenResponse = await $fetch(tokenInfoUrl) + if (typeof tokenResponse === 'string') { + tokenResponse = JSON.parse(tokenResponse) + } + + console.log('[Synology SSO] Token Response:', JSON.stringify(tokenResponse, null, 2)) + + if (!tokenResponse.success) { + throw createError({ + statusCode: 401, + message: 'Synology 사용자 정보를 가져올 수 없습니다.' + }) + } + + // user_name으로 사용자 조회 + const synologyUsername = tokenResponse.data?.user_name + + if (!synologyUsername) { + throw createError({ + statusCode: 401, + message: 'Synology 사용자명을 가져올 수 없습니다.' + }) + } + + console.log('[Synology SSO] Username:', synologyUsername) + + // synology_id나 email의 앞부분으로 매칭 시도 + const synologyEmail = `${synologyUsername}@osolit.net` + + const employee = await queryOne(` + SELECT employee_id, employee_name, is_active, password_hash, + synology_id, synology_email, employee_email + FROM wr_employee_info + WHERE synology_id = $1 + OR employee_email = $2 + `, [synologyUsername, synologyEmail]) + + if (!employee) { + throw createError({ + statusCode: 404, + message: `Synology 계정 "${synologyUsername}"과 연결된 사용자를 찾을 수 없습니다. 관리자에게 문의하세요.` + }) + } + + if (!employee.is_active) { + throw createError({ + statusCode: 403, + message: '비활성화된 계정입니다.' + }) + } + + // 로그인 이력 기록 + await execute(` + INSERT INTO wr_login_history (employee_id, login_type, login_ip, login_at, login_success, login_email) + VALUES ($1, 'SYNOLOGY', $2, NOW(), true, $3) + `, [employee.employee_id, ip, synologyUsername]) + + // 세션 생성 + await createSession(event, employee.employee_id) + + return { + success: true, + needPasswordSet: !employee.password_hash + } } - // DB에서 synology_username으로 사용자 조회 - const users = await query(` - SELECT employee_id, employee_name, employee_email, employee_role, company_id - FROM employees - WHERE synology_username = $1 - AND employee_status = 'active' - LIMIT 1 - `, [synologyUsername]) + // 이메일이 있는 경우 + const synologyEmail = userResponse.data.email + const synologyId = userResponse.data.user_id || userResponse.data.uid - if (users.length === 0) { + console.log('[Synology SSO] Email:', synologyEmail) + + // 이메일로 사용자 매칭 + const employee = await queryOne(` + SELECT employee_id, employee_name, is_active, password_hash, + synology_id, synology_email + FROM wr_employee_info + WHERE employee_email = $1 + `, [synologyEmail]) + + if (!employee) { throw createError({ statusCode: 404, - message: `Synology 계정 "${synologyUsername}"과 연결된 사용자를 찾을 수 없습니다. 관리자에게 문의하세요.` + message: '등록되지 않은 사용자입니다. 관리자에게 문의하세요.' }) } - const user = users[0] + if (!employee.is_active) { + throw createError({ + statusCode: 403, + message: '비활성화된 계정입니다.' + }) + } + + // Synology 계정 연결 정보 업데이트 + await execute(` + UPDATE wr_employee_info + SET synology_id = $1, synology_email = $2, synology_linked_at = NOW() + WHERE employee_id = $3 + `, [synologyId, synologyEmail, employee.employee_id]) + + // 로그인 이력 기록 + await execute(` + INSERT INTO wr_login_history (employee_id, login_type, login_ip, login_at, login_success, login_email) + VALUES ($1, 'SYNOLOGY', $2, NOW(), true, $3) + `, [employee.employee_id, ip, synologyEmail]) // 세션 생성 - const session = await useAuthSession(event) - await session.update({ - userId: user.employee_id, - userName: user.employee_name, - userEmail: user.employee_email, - userRole: user.employee_role, - companyId: user.company_id, - loginType: 'synology' - }) + await createSession(event, employee.employee_id) return { success: true, - user: { - id: user.employee_id, - name: user.employee_name, - email: user.employee_email, - role: user.employee_role - } + needPasswordSet: !employee.password_hash } + } catch (error: any) { + console.error('[Synology SSO] Error:', error) if (error.statusCode) { throw error } - console.error('Synology SSO verify error:', error) throw createError({ statusCode: 500, message: 'Synology 로그인 처리 중 오류가 발생했습니다.'