refactor: Synology SSO 개선 및 불필요한 로그인 API 정리
- Synology 버튼: '오솔NAS로 로그인'으로 변경
- 이메일 매칭: {username}@osolit.net으로 매칭
- 삭제된 API:
- login.post.ts (이메일+이름 로그인)
- recent-users.get.ts (최근 로그인 사용자)
- select-user.post.ts (사용자 선택 로그인)
This commit is contained in:
@@ -17,7 +17,6 @@ onMounted(async () => {
|
|||||||
const hash = window.location.hash.substring(1)
|
const hash = window.location.hash.substring(1)
|
||||||
const params = new URLSearchParams(hash)
|
const params = new URLSearchParams(hash)
|
||||||
const accessToken = params.get('access_token')
|
const accessToken = params.get('access_token')
|
||||||
const state = params.get('state')
|
|
||||||
const error = params.get('error')
|
const error = params.get('error')
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -32,11 +31,16 @@ onMounted(async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 서버로 access_token 전송하여 로그인 처리
|
// 서버로 access_token 전송하여 로그인 처리
|
||||||
await $fetch('/api/auth/synology/verify', {
|
const result = await $fetch<{ success: boolean; needPasswordSet?: boolean }>('/api/auth/synology/verify', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { accessToken, state }
|
body: { accessToken }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (result.needPasswordSet) {
|
||||||
|
router.push('/set-password?from=synology')
|
||||||
|
} else {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
router.push(`/login?error=${encodeURIComponent(e.data?.message || 'Synology 로그인에 실패했습니다.')}`)
|
router.push(`/login?error=${encodeURIComponent(e.data?.message || 'Synology 로그인에 실패했습니다.')}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,9 @@
|
|||||||
Google로 로그인
|
Google로 로그인
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Synology 로그인 -->
|
<!-- 오솔NAS 로그인 -->
|
||||||
<a href="/api/auth/synology" class="btn btn-outline-dark w-100">
|
<a href="/api/auth/synology" class="btn btn-outline-dark w-100">
|
||||||
<i class="bi bi-hdd-network me-2"></i>Synology로 로그인
|
<i class="bi bi-hdd-network me-2"></i>오솔NAS로 로그인
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- 비밀번호 찾기 -->
|
<!-- 비밀번호 찾기 -->
|
||||||
|
|||||||
@@ -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<LoginBody>(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<any>(`
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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<SelectUserBody>(event)
|
|
||||||
const clientIp = getClientIp(event)
|
|
||||||
const userAgent = getHeader(event, 'user-agent') || null
|
|
||||||
|
|
||||||
if (!body.employeeId) {
|
|
||||||
throw createError({ statusCode: 400, message: '사용자를 선택해주세요.' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사원 조회
|
|
||||||
const employee = await queryOne<any>(`
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -2,11 +2,14 @@
|
|||||||
* Synology SSO Access Token 검증 및 로그인 처리
|
* Synology SSO Access Token 검증 및 로그인 처리
|
||||||
* POST /api/auth/synology/verify
|
* 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) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const { accessToken } = body
|
const { accessToken } = body
|
||||||
|
const ip = getClientIp(event)
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -17,71 +20,152 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
console.log('[Synology SSO] Access Token:', accessToken)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Synology SSO Server에서 사용자 정보 조회
|
// 1. 액세스 토큰으로 사용자 정보 조회
|
||||||
const userInfoUrl = `${config.synologyServerUrl}/webman/sso/SSOAccessToken.cgi?action=exchange&access_token=${accessToken}&app_id=${config.synologyClientId}`
|
const userInfoUrl = `${config.synologyServerUrl}/webman/sso/SSOUserInfo.cgi`
|
||||||
|
|
||||||
const userInfoResponse = await $fetch<any>(userInfoUrl)
|
console.log('[Synology SSO] Requesting user info...')
|
||||||
|
|
||||||
if (!userInfoResponse.success) {
|
let userResponse = await $fetch<any>(userInfoUrl, {
|
||||||
throw createError({
|
method: 'GET',
|
||||||
statusCode: 401,
|
headers: {
|
||||||
message: userInfoResponse.error?.msg || 'Synology 토큰 검증에 실패했습니다.'
|
'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) {
|
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<any>(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({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: 'Synology 사용자 정보를 가져올 수 없습니다.'
|
message: 'Synology 사용자 정보를 가져올 수 없습니다.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB에서 synology_username으로 사용자 조회
|
// user_name으로 사용자 조회
|
||||||
const users = await query(`
|
const synologyUsername = tokenResponse.data?.user_name
|
||||||
SELECT employee_id, employee_name, employee_email, employee_role, company_id
|
|
||||||
FROM employees
|
|
||||||
WHERE synology_username = $1
|
|
||||||
AND employee_status = 'active'
|
|
||||||
LIMIT 1
|
|
||||||
`, [synologyUsername])
|
|
||||||
|
|
||||||
if (users.length === 0) {
|
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<any>(`
|
||||||
|
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({
|
throw createError({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
message: `Synology 계정 "${synologyUsername}"과 연결된 사용자를 찾을 수 없습니다. 관리자에게 문의하세요.`
|
message: `Synology 계정 "${synologyUsername}"과 연결된 사용자를 찾을 수 없습니다. 관리자에게 문의하세요.`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = users[0]
|
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])
|
||||||
|
|
||||||
// 세션 생성
|
// 세션 생성
|
||||||
const session = await useAuthSession(event)
|
await createSession(event, employee.employee_id)
|
||||||
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'
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
user: {
|
needPasswordSet: !employee.password_hash
|
||||||
id: user.employee_id,
|
|
||||||
name: user.employee_name,
|
|
||||||
email: user.employee_email,
|
|
||||||
role: user.employee_role
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이메일이 있는 경우
|
||||||
|
const synologyEmail = userResponse.data.email
|
||||||
|
const synologyId = userResponse.data.user_id || userResponse.data.uid
|
||||||
|
|
||||||
|
console.log('[Synology SSO] Email:', synologyEmail)
|
||||||
|
|
||||||
|
// 이메일로 사용자 매칭
|
||||||
|
const employee = await queryOne<any>(`
|
||||||
|
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: '등록되지 않은 사용자입니다. 관리자에게 문의하세요.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
// 세션 생성
|
||||||
|
await createSession(event, employee.employee_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
needPasswordSet: !employee.password_hash
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('[Synology SSO] Error:', error)
|
||||||
if (error.statusCode) {
|
if (error.statusCode) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
console.error('Synology SSO verify error:', error)
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
message: 'Synology 로그인 처리 중 오류가 발생했습니다.'
|
message: 'Synology 로그인 처리 중 오류가 발생했습니다.'
|
||||||
|
|||||||
Reference in New Issue
Block a user