331 lines
9.5 KiB
TypeScript
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]
|
|
)
|
|
}
|
|
}
|