- 역할 생성/수정/삭제 API 추가 (POST/PUT/DELETE /api/roles) - 권한 관리 패널에 역할 추가/이름수정/삭제 UI 구현 - 사용자 관리 패널에 다중 역할 뱃지 표시 및 역할 할당 드롭다운 추가 - 사용자 활성화/비활성화 상태 변경 버튼 추가 - UserListItem에 roleSns 필드 추가로 역할 SN 기반 할당 지원 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
201 lines
5.7 KiB
TypeScript
201 lines
5.7 KiB
TypeScript
import { authPool } from '../db/authDb.js'
|
|
import { AuthError } from '../auth/authService.js'
|
|
|
|
const PERM_RESOURCE_CODES = [
|
|
'prediction', 'hns', 'rescue', 'reports', 'aerial',
|
|
'assets', 'scat', 'incidents', 'board', 'weather', 'admin',
|
|
] as const
|
|
|
|
const PROTECTED_ROLE_CODES = ['ADMIN']
|
|
|
|
interface RoleWithPermissions {
|
|
sn: number
|
|
code: string
|
|
name: string
|
|
description: string | null
|
|
isDefault: boolean
|
|
permissions: Array<{
|
|
sn: number
|
|
resourceCode: string
|
|
granted: boolean
|
|
}>
|
|
}
|
|
|
|
interface CreateRoleInput {
|
|
code: string
|
|
name: string
|
|
description?: string
|
|
}
|
|
|
|
interface UpdateRoleInput {
|
|
name?: string
|
|
description?: string
|
|
}
|
|
|
|
export async function listRolesWithPermissions(): Promise<RoleWithPermissions[]> {
|
|
const rolesResult = await authPool.query(
|
|
`SELECT ROLE_SN as sn, ROLE_CD as code, ROLE_NM as name, ROLE_DC as description, DFLT_YN as is_default
|
|
FROM AUTH_ROLE ORDER BY ROLE_SN`
|
|
)
|
|
|
|
const roles: RoleWithPermissions[] = []
|
|
|
|
for (const row of rolesResult.rows) {
|
|
const permsResult = await authPool.query(
|
|
`SELECT PERM_SN as sn, RSRC_CD as resource_code, GRANT_YN as granted
|
|
FROM AUTH_PERM WHERE ROLE_SN = $1 ORDER BY RSRC_CD`,
|
|
[row.sn]
|
|
)
|
|
|
|
roles.push({
|
|
sn: row.sn,
|
|
code: row.code,
|
|
name: row.name,
|
|
description: row.description,
|
|
isDefault: row.is_default === 'Y',
|
|
permissions: permsResult.rows.map((p: { sn: number; resource_code: string; granted: string }) => ({
|
|
sn: p.sn,
|
|
resourceCode: p.resource_code,
|
|
granted: p.granted === 'Y',
|
|
})),
|
|
})
|
|
}
|
|
|
|
return roles
|
|
}
|
|
|
|
export async function updateRoleDefault(roleSn: number, isDefault: boolean): Promise<void> {
|
|
await authPool.query(
|
|
'UPDATE AUTH_ROLE SET DFLT_YN = $1 WHERE ROLE_SN = $2',
|
|
[isDefault ? 'Y' : 'N', roleSn]
|
|
)
|
|
}
|
|
|
|
export async function createRole(input: CreateRoleInput): Promise<RoleWithPermissions> {
|
|
if (!/^[A-Z][A-Z0-9_]{0,18}[A-Z0-9]$/.test(input.code)) {
|
|
throw new AuthError('역할 코드는 영문 대문자, 숫자, 언더스코어만 허용되며 2~20자입니다.', 400)
|
|
}
|
|
|
|
const client = await authPool.connect()
|
|
try {
|
|
await client.query('BEGIN')
|
|
|
|
const existing = await client.query(
|
|
'SELECT 1 FROM AUTH_ROLE WHERE ROLE_CD = $1', [input.code]
|
|
)
|
|
if (existing.rows.length > 0) {
|
|
throw new AuthError('이미 존재하는 역할 코드입니다.', 409)
|
|
}
|
|
|
|
const result = await client.query(
|
|
`INSERT INTO AUTH_ROLE (ROLE_CD, ROLE_NM, ROLE_DC) VALUES ($1, $2, $3)
|
|
RETURNING ROLE_SN as sn, ROLE_CD as code, ROLE_NM as name, ROLE_DC as description, DFLT_YN as is_default`,
|
|
[input.code, input.name, input.description || null]
|
|
)
|
|
const row = result.rows[0]
|
|
|
|
for (const rsrc of PERM_RESOURCE_CODES) {
|
|
await client.query(
|
|
'INSERT INTO AUTH_PERM (ROLE_SN, RSRC_CD, GRANT_YN) VALUES ($1, $2, $3)',
|
|
[row.sn, rsrc, 'N']
|
|
)
|
|
}
|
|
|
|
await client.query('COMMIT')
|
|
|
|
const permsResult = await authPool.query(
|
|
'SELECT PERM_SN as sn, RSRC_CD as resource_code, GRANT_YN as granted FROM AUTH_PERM WHERE ROLE_SN = $1 ORDER BY RSRC_CD',
|
|
[row.sn]
|
|
)
|
|
|
|
return {
|
|
sn: row.sn,
|
|
code: row.code,
|
|
name: row.name,
|
|
description: row.description,
|
|
isDefault: row.is_default === 'Y',
|
|
permissions: permsResult.rows.map((p: { sn: number; resource_code: string; granted: string }) => ({
|
|
sn: p.sn,
|
|
resourceCode: p.resource_code,
|
|
granted: p.granted === 'Y',
|
|
})),
|
|
}
|
|
} catch (err) {
|
|
await client.query('ROLLBACK')
|
|
throw err
|
|
} finally {
|
|
client.release()
|
|
}
|
|
}
|
|
|
|
export async function updateRole(roleSn: number, input: UpdateRoleInput): Promise<void> {
|
|
const roleResult = await authPool.query(
|
|
'SELECT ROLE_CD FROM AUTH_ROLE WHERE ROLE_SN = $1', [roleSn]
|
|
)
|
|
if (roleResult.rows.length === 0) {
|
|
throw new AuthError('역할을 찾을 수 없습니다.', 404)
|
|
}
|
|
|
|
const sets: string[] = []
|
|
const params: (string | number | null)[] = []
|
|
let idx = 1
|
|
|
|
if (input.name !== undefined) {
|
|
sets.push(`ROLE_NM = $${idx++}`)
|
|
params.push(input.name)
|
|
}
|
|
if (input.description !== undefined) {
|
|
sets.push(`ROLE_DC = $${idx++}`)
|
|
params.push(input.description)
|
|
}
|
|
|
|
if (sets.length === 0) {
|
|
throw new AuthError('수정할 항목이 없습니다.', 400)
|
|
}
|
|
|
|
params.push(roleSn)
|
|
await authPool.query(
|
|
`UPDATE AUTH_ROLE SET ${sets.join(', ')} WHERE ROLE_SN = $${idx}`,
|
|
params
|
|
)
|
|
}
|
|
|
|
export async function deleteRole(roleSn: number): Promise<void> {
|
|
const roleResult = await authPool.query(
|
|
'SELECT ROLE_CD FROM AUTH_ROLE WHERE ROLE_SN = $1', [roleSn]
|
|
)
|
|
if (roleResult.rows.length === 0) {
|
|
throw new AuthError('역할을 찾을 수 없습니다.', 404)
|
|
}
|
|
|
|
if (PROTECTED_ROLE_CODES.includes(roleResult.rows[0].role_cd)) {
|
|
throw new AuthError('시스템 보호 역할은 삭제할 수 없습니다.', 403)
|
|
}
|
|
|
|
await authPool.query('DELETE FROM AUTH_ROLE WHERE ROLE_SN = $1', [roleSn])
|
|
}
|
|
|
|
export async function updatePermissions(
|
|
roleSn: number,
|
|
permissions: Array<{ resourceCode: string; granted: boolean }>
|
|
): Promise<void> {
|
|
for (const perm of permissions) {
|
|
const existing = await authPool.query(
|
|
'SELECT PERM_SN FROM AUTH_PERM WHERE ROLE_SN = $1 AND RSRC_CD = $2',
|
|
[roleSn, perm.resourceCode]
|
|
)
|
|
|
|
if (existing.rows.length > 0) {
|
|
await authPool.query(
|
|
'UPDATE AUTH_PERM SET GRANT_YN = $1 WHERE ROLE_SN = $2 AND RSRC_CD = $3',
|
|
[perm.granted ? 'Y' : 'N', roleSn, perm.resourceCode]
|
|
)
|
|
} else {
|
|
await authPool.query(
|
|
'INSERT INTO AUTH_PERM (ROLE_SN, RSRC_CD, GRANT_YN) VALUES ($1, $2, $3)',
|
|
[roleSn, perm.resourceCode, perm.granted ? 'Y' : 'N']
|
|
)
|
|
}
|
|
}
|
|
}
|