기능구현중
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
import { query, queryOne, execute } from '../../utils/db'
|
||||
import { getClientIp } from '../../utils/ip'
|
||||
import { requireAuth } from '../../utils/session'
|
||||
import { hashPassword, verifyPassword } from '../../utils/password'
|
||||
|
||||
interface ChangePasswordBody {
|
||||
currentPassword?: string
|
||||
newPassword: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
* POST /api/auth/change-password
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const employeeId = await requireAuth(event)
|
||||
|
||||
const body = await readBody<ChangePasswordBody>(event)
|
||||
const clientIp = getClientIp(event)
|
||||
|
||||
if (!body.newPassword || !body.confirmPassword) {
|
||||
throw createError({ statusCode: 400, message: '새 비밀번호를 입력해주세요.' })
|
||||
}
|
||||
|
||||
if (body.newPassword !== body.confirmPassword) {
|
||||
throw createError({ statusCode: 400, message: '비밀번호가 일치하지 않습니다.' })
|
||||
}
|
||||
|
||||
if (body.newPassword.length < 8) {
|
||||
throw createError({ statusCode: 400, message: '비밀번호는 8자 이상이어야 합니다.' })
|
||||
}
|
||||
|
||||
// 현재 직원 정보 조회
|
||||
const employee = await queryOne<any>(`
|
||||
SELECT password_hash, employee_email FROM wr_employee_info WHERE employee_id = $1
|
||||
`, [employeeId])
|
||||
|
||||
if (!employee) {
|
||||
throw createError({ statusCode: 404, message: '사용자를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
// 기존 비밀번호가 있으면 현재 비밀번호 검증
|
||||
if (employee.password_hash) {
|
||||
if (!body.currentPassword) {
|
||||
throw createError({ statusCode: 400, message: '현재 비밀번호를 입력해주세요.' })
|
||||
}
|
||||
const isValid = await verifyPassword(body.currentPassword, employee.password_hash)
|
||||
if (!isValid) {
|
||||
throw createError({ statusCode: 401, message: '현재 비밀번호가 올바르지 않습니다.' })
|
||||
}
|
||||
}
|
||||
|
||||
// 새 비밀번호 해시
|
||||
const newHash = await hashPassword(body.newPassword)
|
||||
|
||||
// 비밀번호 업데이트
|
||||
await execute(`
|
||||
UPDATE wr_employee_info
|
||||
SET password_hash = $1, updated_at = NOW(), updated_ip = $2, updated_email = $3
|
||||
WHERE employee_id = $4
|
||||
`, [newHash, clientIp, employee.employee_email, employeeId])
|
||||
|
||||
return { success: true, message: '비밀번호가 변경되었습니다.' }
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
import { getDbSession, refreshSession, getSessionIdFromCookie, deleteSessionCookie, getUserRoles } from '../../utils/session'
|
||||
import { queryOne, execute, query } from '../../utils/db'
|
||||
|
||||
/**
|
||||
* 현재 로그인 사용자 정보 (권한 포함)
|
||||
* GET /api/auth/current-user
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const sessionId = getSessionIdFromCookie(event)
|
||||
|
||||
if (!sessionId) {
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
// DB에서 세션 조회
|
||||
const session = await getDbSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
// 세션이 만료되었거나 없음 → 쿠키 삭제
|
||||
deleteSessionCookie(event)
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
// 사용자 정보 조회
|
||||
const employee = await queryOne<any>(`
|
||||
SELECT * FROM wr_employee_info
|
||||
WHERE employee_id = $1 AND is_active = true
|
||||
`, [session.employeeId])
|
||||
|
||||
if (!employee) {
|
||||
deleteSessionCookie(event)
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
// 세션 갱신 (Sliding Expiration - 10분 연장)
|
||||
await refreshSession(sessionId)
|
||||
|
||||
// 로그인 이력의 last_active_at도 업데이트
|
||||
if (session.loginHistoryId) {
|
||||
await execute(`
|
||||
UPDATE wr_login_history
|
||||
SET last_active_at = NOW()
|
||||
WHERE history_id = $1
|
||||
`, [session.loginHistoryId])
|
||||
}
|
||||
|
||||
// 사용자 권한 조회
|
||||
const roles = await getUserRoles(employee.employee_id)
|
||||
|
||||
return {
|
||||
user: {
|
||||
employeeId: employee.employee_id,
|
||||
employeeName: employee.employee_name,
|
||||
employeeEmail: employee.employee_email,
|
||||
employeePosition: employee.employee_position,
|
||||
roles // 권한 코드 배열 추가
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,127 +0,0 @@
|
||||
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')
|
||||
}
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Google OAuth 시작
|
||||
* GET /api/auth/google
|
||||
*
|
||||
* Query params:
|
||||
* - extend: 'groups' - 구글 그룹 접근 권한 추가 요청
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const query = getQuery(event)
|
||||
|
||||
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가 설정되지 않았습니다.' })
|
||||
}
|
||||
|
||||
// 기본 scope + 확장 scope
|
||||
let scopes = ['openid', 'email', 'profile']
|
||||
|
||||
// 구글 그룹 권한 요청 시 추가 scope
|
||||
if (query.extend === 'groups') {
|
||||
scopes.push(
|
||||
'https://www.googleapis.com/auth/gmail.readonly', // 그룹 메일 읽기
|
||||
'https://www.googleapis.com/auth/cloud-identity.groups.readonly' // 그룹 정보 읽기
|
||||
)
|
||||
}
|
||||
|
||||
const scope = encodeURIComponent(scopes.join(' '))
|
||||
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)
|
||||
})
|
||||
@@ -1,75 +0,0 @@
|
||||
import { getDbSession, getSessionIdFromCookie, deleteSessionCookie, SESSION_TIMEOUT_MINUTES } from '../../utils/session'
|
||||
|
||||
/**
|
||||
* 본인 로그인 이력 조회
|
||||
* GET /api/auth/login-history
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const sessionId = getSessionIdFromCookie(event)
|
||||
|
||||
if (!sessionId) {
|
||||
throw createError({ statusCode: 401, message: '로그인이 필요합니다.' })
|
||||
}
|
||||
|
||||
const session = await getDbSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
deleteSessionCookie(event)
|
||||
throw createError({ statusCode: 401, message: '세션이 만료되었습니다.' })
|
||||
}
|
||||
|
||||
// 현재 활성 세션 ID 목록 조회
|
||||
const activeSessions = await query<any>(`
|
||||
SELECT login_history_id FROM wr_session
|
||||
WHERE employee_id = $1 AND expires_at > NOW()
|
||||
`, [session.employeeId])
|
||||
|
||||
const activeHistoryIds = new Set(activeSessions.map(s => s.login_history_id))
|
||||
|
||||
// 로그인 이력 조회
|
||||
const history = await query<any>(`
|
||||
SELECT
|
||||
history_id,
|
||||
login_at,
|
||||
login_ip,
|
||||
logout_at,
|
||||
logout_ip,
|
||||
last_active_at
|
||||
FROM wr_login_history
|
||||
WHERE employee_id = $1
|
||||
ORDER BY login_at DESC
|
||||
LIMIT 50
|
||||
`, [session.employeeId])
|
||||
|
||||
const SESSION_TIMEOUT_MS = SESSION_TIMEOUT_MINUTES * 60 * 1000
|
||||
const now = Date.now()
|
||||
|
||||
return {
|
||||
history: history.map(h => {
|
||||
const isCurrentSession = h.history_id === session.loginHistoryId
|
||||
const isActiveSession = activeHistoryIds.has(h.history_id)
|
||||
|
||||
// 세션 상태 판단
|
||||
let sessionStatus: 'active' | 'logout' | 'expired'
|
||||
if (h.logout_at) {
|
||||
sessionStatus = 'logout'
|
||||
} else if (isActiveSession) {
|
||||
sessionStatus = 'active'
|
||||
} else {
|
||||
// 활성 세션에 없으면 만료
|
||||
sessionStatus = 'expired'
|
||||
}
|
||||
|
||||
return {
|
||||
historyId: h.history_id,
|
||||
loginAt: h.login_at,
|
||||
loginIp: h.login_ip,
|
||||
logoutAt: h.logout_at,
|
||||
logoutIp: h.logout_ip,
|
||||
lastActiveAt: h.last_active_at,
|
||||
isCurrentSession,
|
||||
sessionStatus
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
import { query, execute, insertReturning } from '../../utils/db'
|
||||
import { getClientIp } from '../../utils/ip'
|
||||
import { createSession, setSessionCookie } from '../../utils/session'
|
||||
import { verifyPassword } from '../../utils/password'
|
||||
|
||||
interface LoginBody {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 로그인
|
||||
* POST /api/auth/login-password
|
||||
*/
|
||||
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.password) {
|
||||
throw createError({ statusCode: 400, message: '이메일과 비밀번호를 입력해주세요.' })
|
||||
}
|
||||
|
||||
const emailLower = body.email.toLowerCase()
|
||||
|
||||
// 직원 조회
|
||||
const employees = await query<any>(`
|
||||
SELECT * FROM wr_employee_info WHERE employee_email = $1 AND is_active = true
|
||||
`, [emailLower])
|
||||
|
||||
if (employees.length === 0) {
|
||||
throw createError({ statusCode: 401, message: '이메일 또는 비밀번호가 올바르지 않습니다.' })
|
||||
}
|
||||
|
||||
const employee = employees[0]
|
||||
|
||||
// 비밀번호 미설정
|
||||
if (!employee.password_hash) {
|
||||
throw createError({ statusCode: 401, message: '비밀번호가 설정되지 않았습니다. 관리자에게 문의하세요.' })
|
||||
}
|
||||
|
||||
// 비밀번호 검증
|
||||
const isValid = await verifyPassword(body.password, employee.password_hash)
|
||||
if (!isValid) {
|
||||
throw createError({ statusCode: 401, message: '이메일 또는 비밀번호가 올바르지 않습니다.' })
|
||||
}
|
||||
|
||||
// 마지막 로그인 시간 업데이트
|
||||
await execute(`
|
||||
UPDATE wr_employee_info
|
||||
SET last_login_at = NOW(), last_login_ip = $1, updated_at = NOW()
|
||||
WHERE employee_id = $2
|
||||
`, [clientIp, employee.employee_id])
|
||||
|
||||
// 로그인 이력 추가
|
||||
const loginHistory = await insertReturning(`
|
||||
INSERT INTO wr_login_history (employee_id, login_ip, login_email, login_type)
|
||||
VALUES ($1, $2, $3, 'PASSWORD')
|
||||
RETURNING history_id
|
||||
`, [employee.employee_id, clientIp, emailLower])
|
||||
|
||||
// 세션 생성
|
||||
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,
|
||||
company: employee.company
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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,33 +0,0 @@
|
||||
import { getClientIp } from '../../utils/ip'
|
||||
import { getDbSession, deleteSession, getSessionIdFromCookie, deleteSessionCookie } from '../../utils/session'
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
* POST /api/auth/logout
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const sessionId = getSessionIdFromCookie(event)
|
||||
const clientIp = getClientIp(event)
|
||||
|
||||
if (sessionId) {
|
||||
// 세션 정보 조회
|
||||
const session = await getDbSession(sessionId)
|
||||
|
||||
// 로그아웃 이력 기록
|
||||
if (session?.loginHistoryId) {
|
||||
await execute(`
|
||||
UPDATE wr_login_history
|
||||
SET logout_at = NOW(), logout_ip = $1
|
||||
WHERE history_id = $2
|
||||
`, [clientIp, session.loginHistoryId])
|
||||
}
|
||||
|
||||
// DB에서 세션 삭제
|
||||
await deleteSession(sessionId)
|
||||
}
|
||||
|
||||
// 세션 쿠키 삭제
|
||||
deleteSessionCookie(event)
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
@@ -1,70 +0,0 @@
|
||||
import { getDbSession, getSessionIdFromCookie, deleteSessionCookie } from '../../utils/session'
|
||||
|
||||
/**
|
||||
* 로그인된 사용자 상세 정보 조회
|
||||
* GET /api/auth/me
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const sessionId = getSessionIdFromCookie(event)
|
||||
|
||||
if (!sessionId) {
|
||||
throw createError({ statusCode: 401, message: '로그인이 필요합니다.' })
|
||||
}
|
||||
|
||||
// DB에서 세션 조회
|
||||
const session = await getDbSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
deleteSessionCookie(event)
|
||||
throw createError({ statusCode: 401, message: '세션이 만료되었습니다. 다시 로그인해주세요.' })
|
||||
}
|
||||
|
||||
const employee = await queryOne<any>(`
|
||||
SELECT
|
||||
employee_id,
|
||||
employee_name,
|
||||
employee_email,
|
||||
employee_phone,
|
||||
employee_position,
|
||||
company,
|
||||
join_date,
|
||||
is_active,
|
||||
created_at,
|
||||
created_ip,
|
||||
updated_at,
|
||||
updated_ip,
|
||||
password_hash,
|
||||
google_id,
|
||||
google_email,
|
||||
synology_id,
|
||||
synology_email
|
||||
FROM wr_employee_info
|
||||
WHERE employee_id = $1
|
||||
`, [session.employeeId])
|
||||
|
||||
if (!employee) {
|
||||
throw createError({ statusCode: 404, message: '사용자를 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
employeeId: employee.employee_id,
|
||||
employeeName: employee.employee_name,
|
||||
employeeEmail: employee.employee_email,
|
||||
employeePhone: employee.employee_phone,
|
||||
employeePosition: employee.employee_position,
|
||||
company: employee.company,
|
||||
joinDate: employee.join_date,
|
||||
isActive: employee.is_active,
|
||||
createdAt: employee.created_at,
|
||||
createdIp: employee.created_ip,
|
||||
updatedAt: employee.updated_at,
|
||||
updatedIp: employee.updated_ip,
|
||||
hasPassword: !!employee.password_hash,
|
||||
googleId: employee.google_id,
|
||||
googleEmail: employee.google_email,
|
||||
synologyId: employee.synology_id,
|
||||
synologyEmail: employee.synology_email
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,80 +0,0 @@
|
||||
import { query } from '../../utils/db'
|
||||
import { getDbSession, getSessionIdFromCookie } from '../../utils/session'
|
||||
|
||||
/**
|
||||
* 현재 사용자 접근 가능 메뉴 조회
|
||||
* GET /api/auth/menu
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const sessionId = getSessionIdFromCookie(event)
|
||||
|
||||
if (!sessionId) {
|
||||
throw createError({ statusCode: 401, message: '로그인이 필요합니다.' })
|
||||
}
|
||||
|
||||
const session = await getDbSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
throw createError({ statusCode: 401, message: '세션이 만료되었습니다.' })
|
||||
}
|
||||
|
||||
// 사용자의 권한 목록 조회
|
||||
const userRoles = await query<any>(`
|
||||
SELECT r.role_id, r.role_code
|
||||
FROM wr_employee_role er
|
||||
JOIN wr_role r ON er.role_id = r.role_id
|
||||
WHERE er.employee_id = $1
|
||||
`, [session.employeeId])
|
||||
|
||||
const roleIds = userRoles.map(r => r.role_id)
|
||||
|
||||
if (roleIds.length === 0) {
|
||||
return { menus: [] }
|
||||
}
|
||||
|
||||
// 접근 가능한 메뉴 조회
|
||||
const menus = await query<any>(`
|
||||
SELECT DISTINCT
|
||||
m.menu_id,
|
||||
m.menu_code,
|
||||
m.menu_name,
|
||||
m.menu_path,
|
||||
m.menu_icon,
|
||||
m.parent_menu_id,
|
||||
m.sort_order
|
||||
FROM wr_menu m
|
||||
JOIN wr_menu_role mr ON m.menu_id = mr.menu_id
|
||||
WHERE mr.role_id = ANY($1)
|
||||
AND m.is_active = true
|
||||
ORDER BY m.parent_menu_id NULLS FIRST, m.sort_order
|
||||
`, [roleIds])
|
||||
|
||||
// 계층 구조로 변환
|
||||
const menuMap = new Map<number, any>()
|
||||
const rootMenus: any[] = []
|
||||
|
||||
for (const m of menus) {
|
||||
const menuItem = {
|
||||
menuId: m.menu_id,
|
||||
menuCode: m.menu_code,
|
||||
menuName: m.menu_name,
|
||||
menuPath: m.menu_path,
|
||||
menuIcon: m.menu_icon,
|
||||
parentMenuId: m.parent_menu_id,
|
||||
sortOrder: m.sort_order,
|
||||
children: []
|
||||
}
|
||||
menuMap.set(m.menu_id, menuItem)
|
||||
}
|
||||
|
||||
for (const m of menus) {
|
||||
const menuItem = menuMap.get(m.menu_id)
|
||||
if (m.parent_menu_id && menuMap.has(m.parent_menu_id)) {
|
||||
menuMap.get(m.parent_menu_id).children.push(menuItem)
|
||||
} else if (!m.parent_menu_id) {
|
||||
rootMenus.push(menuItem)
|
||||
}
|
||||
}
|
||||
|
||||
return { menus: rootMenus }
|
||||
})
|
||||
@@ -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,78 +0,0 @@
|
||||
import { query, execute } from '../../utils/db'
|
||||
import { hashPassword, generateTempPassword } from '../../utils/password'
|
||||
import { sendTempPasswordEmail } from '../../utils/email'
|
||||
import { getClientIp } from '../../utils/ip'
|
||||
|
||||
interface ResetPasswordBody {
|
||||
email: string
|
||||
name: string
|
||||
phone?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 찾기 (임시 비밀번호 발급)
|
||||
* POST /api/auth/reset-password
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<ResetPasswordBody>(event)
|
||||
const clientIp = getClientIp(event)
|
||||
|
||||
if (!body.email || !body.name) {
|
||||
throw createError({ statusCode: 400, message: '이메일과 이름을 입력해주세요.' })
|
||||
}
|
||||
|
||||
const emailLower = body.email.toLowerCase()
|
||||
const nameTrimmed = body.name.trim()
|
||||
|
||||
// 사용자 조회 (이메일 + 이름)
|
||||
let sql = `SELECT * FROM wr_employee_info WHERE employee_email = $1 AND employee_name = $2`
|
||||
const params: any[] = [emailLower, nameTrimmed]
|
||||
|
||||
// 핸드폰 번호도 제공된 경우 추가 검증
|
||||
if (body.phone) {
|
||||
const phoneClean = body.phone.replace(/[^0-9]/g, '')
|
||||
sql += ` AND REPLACE(employee_phone, '-', '') = $3`
|
||||
params.push(phoneClean)
|
||||
}
|
||||
|
||||
const employees = await query<any>(sql, params)
|
||||
|
||||
if (employees.length === 0) {
|
||||
// 보안상 동일한 메시지 반환
|
||||
throw createError({ statusCode: 400, message: '입력하신 정보와 일치하는 계정을 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
const employee = employees[0]
|
||||
|
||||
// 임시 비밀번호 생성
|
||||
const tempPassword = generateTempPassword()
|
||||
const hash = await hashPassword(tempPassword)
|
||||
|
||||
// 비밀번호 업데이트
|
||||
await execute(`
|
||||
UPDATE wr_employee_info
|
||||
SET password_hash = $1, updated_at = NOW(), updated_ip = $2
|
||||
WHERE employee_id = $3
|
||||
`, [hash, clientIp, employee.employee_id])
|
||||
|
||||
// 이메일 발송
|
||||
const emailSent = await sendTempPasswordEmail(
|
||||
employee.employee_email,
|
||||
employee.employee_name,
|
||||
tempPassword
|
||||
)
|
||||
|
||||
if (!emailSent) {
|
||||
// 이메일 발송 실패해도 비밀번호는 이미 변경됨
|
||||
console.warn('임시 비밀번호 이메일 발송 실패:', employee.employee_email)
|
||||
}
|
||||
|
||||
// 보안상 이메일 일부만 표시
|
||||
const maskedEmail = employee.employee_email.replace(/(.{2})(.*)(@.*)/, '$1***$3')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `임시 비밀번호가 ${maskedEmail}로 발송되었습니다.`,
|
||||
emailSent
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,77 +0,0 @@
|
||||
import { query, queryOne, execute } from '../../utils/db'
|
||||
import { getClientIp } from '../../utils/ip'
|
||||
import { requireAuth } from '../../utils/session'
|
||||
import { hashPassword, generateTempPassword } from '../../utils/password'
|
||||
|
||||
interface SetPasswordBody {
|
||||
password?: string
|
||||
employeeId?: number // 관리자가 다른 사용자 설정 시
|
||||
generateTemp?: boolean // 임시 비밀번호 생성
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 설정
|
||||
* - 본인: password만 전송
|
||||
* - 관리자: employeeId + (password 또는 generateTemp)
|
||||
* POST /api/auth/set-password
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const currentUserId = await requireAuth(event)
|
||||
|
||||
const body = await readBody<SetPasswordBody>(event)
|
||||
const clientIp = getClientIp(event)
|
||||
|
||||
// 대상 직원 ID 결정 (없으면 본인)
|
||||
let targetEmployeeId = body.employeeId || currentUserId
|
||||
|
||||
// 다른 사람 비밀번호 설정 시 관리자 권한 확인
|
||||
if (body.employeeId && body.employeeId !== currentUserId) {
|
||||
const roles = await query<any>(`
|
||||
SELECT r.role_code FROM wr_employee_role er
|
||||
JOIN wr_role r ON er.role_id = r.role_id
|
||||
WHERE er.employee_id = $1
|
||||
`, [currentUserId])
|
||||
|
||||
const isAdmin = roles.some((r: any) => r.role_code === 'ROLE_ADMIN')
|
||||
if (!isAdmin) {
|
||||
throw createError({ statusCode: 403, message: '관리자 권한이 필요합니다.' })
|
||||
}
|
||||
}
|
||||
|
||||
// 대상 직원 조회
|
||||
const targetEmployee = await queryOne<any>(`
|
||||
SELECT employee_id, employee_name, employee_email FROM wr_employee_info WHERE employee_id = $1
|
||||
`, [targetEmployeeId])
|
||||
|
||||
if (!targetEmployee) {
|
||||
throw createError({ statusCode: 404, message: '직원을 찾을 수 없습니다.' })
|
||||
}
|
||||
|
||||
// 비밀번호 결정
|
||||
let password = body.password
|
||||
if (body.generateTemp || !password) {
|
||||
password = generateTempPassword()
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
throw createError({ statusCode: 400, message: '비밀번호는 8자 이상이어야 합니다.' })
|
||||
}
|
||||
|
||||
// 비밀번호 해시
|
||||
const hash = await hashPassword(password)
|
||||
|
||||
// 업데이트
|
||||
await execute(`
|
||||
UPDATE wr_employee_info
|
||||
SET password_hash = $1, updated_at = NOW(), updated_ip = $2
|
||||
WHERE employee_id = $3
|
||||
`, [hash, clientIp, targetEmployeeId])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
employeeId: targetEmployee.employee_id,
|
||||
employeeName: targetEmployee.employee_name,
|
||||
tempPassword: body.generateTemp ? password : undefined,
|
||||
message: body.generateTemp ? '임시 비밀번호가 생성되었습니다.' : '비밀번호가 설정되었습니다.'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user