wing-ops/backend/src/users/userService.ts
2026-03-25 18:17:42 +09:00

331 lines
9.5 KiB
TypeScript

import { authPool } from '../db/authDb.js'
import { hashPassword, AuthError } from '../auth/authService.js'
import { getSettingBoolean } from '../settings/settingsService.js'
interface UserListItem {
id: string
account: string
name: string
rank: string | null
orgSn: number | null
orgName: string | null
orgAbbr: string | null
status: string
failCount: number
lastLogin: string | null
roles: string[]
roleSns: number[]
regDtm: string
oauthProvider: string | null
email: string | null
}
interface CreateUserInput {
account: string
password: string
name: string
rank?: string
orgSn?: number
roleSns?: number[]
}
interface UpdateUserInput {
name?: string
rank?: string
orgSn?: number | null
status?: string
}
export async function listUsers(search?: string, status?: string): Promise<UserListItem[]> {
let query = `
SELECT u.USER_ID as id, u.USER_ACNT as account, u.USER_NM as name,
u.RNKP_NM as rank, u.ORG_SN as org_sn,
o.ORG_NM as org_name, o.ORG_ABBR_NM as org_abbr,
u.USER_STTS_CD as status, u.FAIL_CNT as fail_count,
u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm,
u.OAUTH_PROVIDER as oauth_provider, u.EMAIL as email
FROM AUTH_USER u
LEFT JOIN AUTH_ORG o ON u.ORG_SN = o.ORG_SN
WHERE 1=1
`
const params: (string | undefined)[] = []
let paramIdx = 1
if (search) {
query += ` AND (u.USER_ACNT ILIKE $${paramIdx} OR u.USER_NM ILIKE $${paramIdx})`
params.push(`%${search}%`)
paramIdx++
}
if (status) {
query += ` AND u.USER_STTS_CD = $${paramIdx}`
params.push(status)
paramIdx++
}
query += ' ORDER BY u.REG_DTM DESC'
const result = await authPool.query(query, params)
const users: UserListItem[] = []
for (const row of result.rows) {
const rolesResult = await authPool.query(
`SELECT r.ROLE_CD, r.ROLE_SN FROM AUTH_USER_ROLE ur JOIN AUTH_ROLE r ON ur.ROLE_SN = r.ROLE_SN WHERE ur.USER_ID = $1`,
[row.id]
)
users.push({
id: row.id,
account: row.account,
name: row.name,
rank: row.rank,
orgSn: row.org_sn,
orgName: row.org_name,
orgAbbr: row.org_abbr,
status: row.status,
failCount: row.fail_count,
lastLogin: row.last_login,
roles: rolesResult.rows.map((r: { role_cd: string }) => r.role_cd),
roleSns: rolesResult.rows.map((r: { role_sn: number }) => r.role_sn),
regDtm: row.reg_dtm,
oauthProvider: row.oauth_provider,
email: row.email,
})
}
return users
}
export async function getUser(userId: string): Promise<UserListItem> {
const result = await authPool.query(
`SELECT u.USER_ID as id, u.USER_ACNT as account, u.USER_NM as name,
u.RNKP_NM as rank, u.ORG_SN as org_sn,
o.ORG_NM as org_name, o.ORG_ABBR_NM as org_abbr,
u.USER_STTS_CD as status, u.FAIL_CNT as fail_count,
u.LAST_LOGIN_DTM as last_login, u.REG_DTM as reg_dtm,
u.OAUTH_PROVIDER as oauth_provider, u.EMAIL as email
FROM AUTH_USER u
LEFT JOIN AUTH_ORG o ON u.ORG_SN = o.ORG_SN
WHERE u.USER_ID = $1`,
[userId]
)
if (result.rows.length === 0) {
throw new AuthError('사용자를 찾을 수 없습니다.', 404)
}
const row = result.rows[0]
const rolesResult = await authPool.query(
`SELECT r.ROLE_CD, r.ROLE_SN FROM AUTH_USER_ROLE ur JOIN AUTH_ROLE r ON ur.ROLE_SN = r.ROLE_SN WHERE ur.USER_ID = $1`,
[userId]
)
return {
id: row.id,
account: row.account,
name: row.name,
rank: row.rank,
orgSn: row.org_sn,
orgName: row.org_name,
orgAbbr: row.org_abbr,
status: row.status,
failCount: row.fail_count,
lastLogin: row.last_login,
roles: rolesResult.rows.map((r: { role_cd: string }) => r.role_cd),
roleSns: rolesResult.rows.map((r: { role_sn: number }) => r.role_sn),
regDtm: row.reg_dtm,
oauthProvider: row.oauth_provider,
email: row.email,
}
}
export async function createUser(input: CreateUserInput): Promise<{ id: string }> {
const existing = await authPool.query(
'SELECT 1 FROM AUTH_USER WHERE USER_ACNT = $1',
[input.account]
)
if (existing.rows.length > 0) {
throw new AuthError('이미 존재하는 계정입니다.', 409)
}
const pswdHash = await hashPassword(input.password)
// 자동 승인 설정 확인
const autoApprove = await getSettingBoolean('registration.auto-approve', true)
const initialStatus = autoApprove ? 'ACTIVE' : 'PENDING'
const result = await authPool.query(
`INSERT INTO AUTH_USER (USER_ACNT, PSWD_HASH, USER_NM, RNKP_NM, ORG_SN, USER_STTS_CD)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING USER_ID as id`,
[input.account, pswdHash, input.name, input.rank || null, input.orgSn || null, initialStatus]
)
const userId = result.rows[0].id
// 역할 할당
if (input.roleSns && input.roleSns.length > 0) {
// 명시적으로 역할 지정된 경우
for (const roleSn of input.roleSns) {
await authPool.query(
'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)',
[userId, roleSn]
)
}
} else {
// 기본 역할 자동 할당 설정 확인
const useDefaultRole = await getSettingBoolean('registration.default-role', true)
if (useDefaultRole) {
const defaultRoles = await authPool.query(
"SELECT ROLE_SN FROM AUTH_ROLE WHERE DFLT_YN = 'Y'"
)
for (const row of defaultRoles.rows) {
await authPool.query(
'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)',
[userId, row.role_sn]
)
}
}
}
return { id: userId }
}
export async function approveUser(userId: string): Promise<void> {
const result = await authPool.query(
'SELECT USER_STTS_CD FROM AUTH_USER WHERE USER_ID = $1',
[userId]
)
if (result.rows.length === 0) {
throw new AuthError('사용자를 찾을 수 없습니다.', 404)
}
if (result.rows[0].user_stts_cd !== 'PENDING') {
throw new AuthError('승인 대기 상태의 사용자만 승인할 수 있습니다.', 400)
}
await authPool.query(
"UPDATE AUTH_USER SET USER_STTS_CD = 'ACTIVE', MDFCN_DTM = NOW() WHERE USER_ID = $1",
[userId]
)
// 기본 역할이 아직 할당되지 않았으면 할당
const existingRoles = await authPool.query(
'SELECT 1 FROM AUTH_USER_ROLE WHERE USER_ID = $1',
[userId]
)
if (existingRoles.rows.length === 0) {
const useDefaultRole = await getSettingBoolean('registration.default-role', true)
if (useDefaultRole) {
const defaultRoles = await authPool.query(
"SELECT ROLE_SN FROM AUTH_ROLE WHERE DFLT_YN = 'Y'"
)
for (const row of defaultRoles.rows) {
await authPool.query(
'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)',
[userId, row.role_sn]
)
}
}
}
}
export async function rejectUser(userId: string): Promise<void> {
const result = await authPool.query(
'SELECT USER_STTS_CD FROM AUTH_USER WHERE USER_ID = $1',
[userId]
)
if (result.rows.length === 0) {
throw new AuthError('사용자를 찾을 수 없습니다.', 404)
}
if (result.rows[0].user_stts_cd !== 'PENDING') {
throw new AuthError('승인 대기 상태의 사용자만 거절할 수 있습니다.', 400)
}
await authPool.query(
"UPDATE AUTH_USER SET USER_STTS_CD = 'REJECTED', MDFCN_DTM = NOW() WHERE USER_ID = $1",
[userId]
)
}
export async function updateUser(userId: string, input: UpdateUserInput): Promise<void> {
const sets: string[] = []
const params: (string | number | null)[] = []
let idx = 1
if (input.name !== undefined) {
sets.push(`USER_NM = $${idx++}`)
params.push(input.name)
}
if (input.rank !== undefined) {
sets.push(`RNKP_NM = $${idx++}`)
params.push(input.rank)
}
if (input.orgSn !== undefined) {
sets.push(`ORG_SN = $${idx++}`)
params.push(input.orgSn)
}
if (input.status !== undefined) {
sets.push(`USER_STTS_CD = $${idx++}`)
params.push(input.status)
if (input.status === 'ACTIVE') {
sets.push('FAIL_CNT = 0')
}
}
if (sets.length === 0) {
throw new AuthError('수정할 항목이 없습니다.', 400)
}
sets.push('MDFCN_DTM = NOW()')
params.push(userId)
await authPool.query(
`UPDATE AUTH_USER SET ${sets.join(', ')} WHERE USER_ID = $${idx}`,
params
)
}
export async function changePassword(userId: string, newPassword: string): Promise<void> {
const pswdHash = await hashPassword(newPassword)
await authPool.query(
'UPDATE AUTH_USER SET PSWD_HASH = $1, MDFCN_DTM = NOW() WHERE USER_ID = $2',
[pswdHash, userId]
)
}
// ── 조직 목록 조회 ──
interface OrgItem {
orgSn: number
orgNm: string
orgAbbrNm: string | null
orgTpCd: string
upperOrgSn: number | null
}
export async function listOrgs(): Promise<OrgItem[]> {
const { rows } = await authPool.query(
`SELECT ORG_SN, ORG_NM, ORG_ABBR_NM, ORG_TP_CD, UPPER_ORG_SN
FROM AUTH_ORG
ORDER BY ORG_SN`
)
return rows.map((r: Record<string, unknown>) => ({
orgSn: r.org_sn as number,
orgNm: r.org_nm as string,
orgAbbrNm: r.org_abbr_nm as string | null,
orgTpCd: r.org_tp_cd as string,
upperOrgSn: r.upper_org_sn as number | null,
}))
}
export async function assignRoles(userId: string, roleSns: number[]): Promise<void> {
await authPool.query('DELETE FROM AUTH_USER_ROLE WHERE USER_ID = $1', [userId])
for (const roleSn of roleSns) {
await authPool.query(
'INSERT INTO AUTH_USER_ROLE (USER_ID, ROLE_SN) VALUES ($1, $2)',
[userId, roleSn]
)
}
}