feat: add Node ACS admin workflows
This commit is contained in:
119
server/routes/security.ts
Normal file
119
server/routes/security.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { Request } from 'express';
|
||||
import { ApiError } from '../http/errors.js';
|
||||
import { query } from '../db/pool.js';
|
||||
|
||||
export interface UserRow {
|
||||
id: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
full_name: string;
|
||||
email: string | null;
|
||||
department: string | null;
|
||||
team_id?: string | null;
|
||||
team_code?: string | null;
|
||||
team_name?: string | null;
|
||||
must_change_password: boolean;
|
||||
enabled: boolean;
|
||||
locked: boolean;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export type QueryFn = typeof query;
|
||||
|
||||
export async function findUserByUsername(dbQuery: QueryFn, username: string): Promise<UserRow | undefined> {
|
||||
const result = await dbQuery<UserRow>(
|
||||
`
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.password_hash,
|
||||
u.full_name,
|
||||
u.email,
|
||||
u.department,
|
||||
u.team_id,
|
||||
t.code AS team_code,
|
||||
t.name AS team_name,
|
||||
u.must_change_password,
|
||||
u.enabled,
|
||||
u.locked,
|
||||
COALESCE(array_agg(ur.role) FILTER (WHERE ur.role IS NOT NULL), '{}') AS roles
|
||||
FROM users u
|
||||
LEFT JOIN teams t ON t.id = u.team_id
|
||||
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
||||
WHERE u.username = $1
|
||||
GROUP BY u.id, t.id
|
||||
`,
|
||||
[username],
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
export async function findUserById(dbQuery: QueryFn, id: number): Promise<UserRow | undefined> {
|
||||
const result = await dbQuery<UserRow>(
|
||||
`
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.password_hash,
|
||||
u.full_name,
|
||||
u.email,
|
||||
u.department,
|
||||
u.team_id,
|
||||
t.code AS team_code,
|
||||
t.name AS team_name,
|
||||
u.must_change_password,
|
||||
u.enabled,
|
||||
u.locked,
|
||||
COALESCE(array_agg(ur.role) FILTER (WHERE ur.role IS NOT NULL), '{}') AS roles
|
||||
FROM users u
|
||||
LEFT JOIN teams t ON t.id = u.team_id
|
||||
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
||||
WHERE u.id = $1
|
||||
GROUP BY u.id, t.id
|
||||
`,
|
||||
[id],
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
export async function requireCurrentUser(dbQuery: QueryFn, req: Request): Promise<UserRow> {
|
||||
const userId = req.session.userId;
|
||||
if (!userId) {
|
||||
throw new ApiError(401, '로그인이 필요합니다.');
|
||||
}
|
||||
|
||||
const user = await findUserById(dbQuery, userId);
|
||||
if (!user || !user.enabled || user.locked) {
|
||||
req.session.userId = undefined;
|
||||
throw new ApiError(401, '로그인이 필요합니다.');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function requireAdmin(dbQuery: QueryFn, req: Request): Promise<UserRow> {
|
||||
const user = await requireCurrentUser(dbQuery, req);
|
||||
if (!user.roles.includes('ADMIN')) {
|
||||
throw new ApiError(403, '접근 권한이 없습니다.');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
export function parseRole(value: string): string {
|
||||
const role = value.trim().toUpperCase() === 'USER' ? 'HOST' : value.trim().toUpperCase();
|
||||
if (!['ADMIN', 'SECURITY', 'HOST'].includes(role)) {
|
||||
throw new ApiError(400, `알 수 없는 권한입니다: ${value}`);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
export function parseRoles(values: unknown): string[] {
|
||||
if (!Array.isArray(values)) {
|
||||
throw new ApiError(400, '권한은 배열로 입력해야 합니다.');
|
||||
}
|
||||
const roles = [...new Set(values.map((value) => parseRole(String(value))))];
|
||||
if (roles.length === 0) {
|
||||
throw new ApiError(400, '권한은 최소 1개 이상 필요합니다.');
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
Reference in New Issue
Block a user