Support revoking admin role via setAdminRole
- appointAdmin을 setAdminRole로 교체 — isAdmin boolean 인자로 부여와 해제를 모두 처리
- 자기 자신의 관리자 권한 해제는 409(CANNOT_REMOVE_SELF_ADMIN)로 차단
- 비활성 유저 검사는 권한 부여 시에만 적용, 응답 형태를 {isAdmin, changed}로 변경
- 어드민 핸들러 라우트와 테스트를 새 시그니처로 갱신
This commit is contained in:
parent
ffafca1338
commit
da8ecf6d4f
@ -12,7 +12,7 @@ import { Timestamp } from "firebase-admin/firestore";
|
||||
import type { OrderStatus, ProductDoc } from "../types/reward";
|
||||
import { transitionOrder, updateOrderShipment } from "../services/orderService";
|
||||
import { invalidateRewardCatalog } from "../services/rewardCatalogService";
|
||||
import { appointAdmin, listAdminUsers, searchAdminUsers } from "../services/adminUserService";
|
||||
import { listAdminUsers, searchAdminUsers, setAdminRole } from "../services/adminUserService";
|
||||
import {
|
||||
toAdminProductDto, toLedgerEntryDto, toOrderDto, toWalletDto,
|
||||
type AdminProductDto, type LedgerPageDto, type OrderPageDto,
|
||||
@ -133,7 +133,7 @@ export const admin = onRequest(async (req, res) => {
|
||||
}
|
||||
|
||||
if (tail === "users/admin" && req.method === "POST") {
|
||||
res.json(await appointAdmin(req.body?.uid, adminUid));
|
||||
res.json(await setAdminRole(req.body?.uid, req.body?.isAdmin, adminUid));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -27,13 +27,14 @@ async function attachAdminStatus(users: AdminUserDto[]): Promise<AdminUserDto[]>
|
||||
|
||||
export interface AdminRoleUpdateResult {
|
||||
uid: string;
|
||||
isAdmin: true;
|
||||
alreadyAdmin: boolean;
|
||||
isAdmin: boolean;
|
||||
changed: boolean;
|
||||
}
|
||||
|
||||
/** 기존 custom claims를 보존하면서 활성 유저에게 관리자 권한을 부여한다. */
|
||||
export async function appointAdmin(
|
||||
/** 기존 custom claims를 보존하면서 관리자 권한을 부여하거나 해제한다. */
|
||||
export async function setAdminRole(
|
||||
uidRaw: unknown,
|
||||
isAdminRaw: unknown,
|
||||
actorUid: string
|
||||
): Promise<AdminRoleUpdateResult> {
|
||||
if (
|
||||
@ -45,23 +46,30 @@ export async function appointAdmin(
|
||||
) {
|
||||
throw new HttpError(400, "invalid uid", "INVALID_INPUT");
|
||||
}
|
||||
if (typeof isAdminRaw !== "boolean") {
|
||||
throw new HttpError(400, "isAdmin must be boolean", "INVALID_INPUT");
|
||||
}
|
||||
if (!isAdminRaw && uidRaw === actorUid) {
|
||||
throw new HttpError(409, "cannot remove own admin role", "CANNOT_REMOVE_SELF_ADMIN");
|
||||
}
|
||||
const user = await getUser(uidRaw);
|
||||
if (!user) throw new HttpError(404, "user not found", "USER_NOT_FOUND");
|
||||
if (user.active === false) {
|
||||
if (isAdminRaw && user.active === false) {
|
||||
throw new HttpError(409, "inactive user cannot be admin", "INACTIVE_USER");
|
||||
}
|
||||
|
||||
const record = await auth.getUser(uidRaw).catch(() => {
|
||||
throw new HttpError(404, "auth user not found", "USER_NOT_FOUND");
|
||||
});
|
||||
if (record.customClaims?.admin === true) {
|
||||
return { uid: uidRaw, isAdmin: true, alreadyAdmin: true };
|
||||
const currentIsAdmin = record.customClaims?.admin === true;
|
||||
if (currentIsAdmin === isAdminRaw) {
|
||||
return { uid: uidRaw, isAdmin: isAdminRaw, changed: false };
|
||||
}
|
||||
await auth.setCustomUserClaims(uidRaw, {
|
||||
...(record.customClaims ?? {}),
|
||||
admin: true,
|
||||
});
|
||||
return { uid: uidRaw, isAdmin: true, alreadyAdmin: false };
|
||||
const nextClaims: Record<string, unknown> = { ...(record.customClaims ?? {}) };
|
||||
if (isAdminRaw) nextClaims.admin = true;
|
||||
else delete nextClaims.admin;
|
||||
await auth.setCustomUserClaims(uidRaw, nextClaims);
|
||||
return { uid: uidRaw, isAdmin: isAdminRaw, changed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { Timestamp } from "firebase-admin/firestore";
|
||||
import { auth, firestore } from "../../src/firebase";
|
||||
import { appointAdmin, listAdminUsers, searchAdminUsers } from "../../src/services/adminUserService";
|
||||
import { listAdminUsers, searchAdminUsers, setAdminRole } from "../../src/services/adminUserService";
|
||||
|
||||
// 다른 테스트가 만드는 users 문서와 섞이지 않도록 고유 접두어를 쓴다.
|
||||
const PREFIX = "검색전용";
|
||||
@ -71,34 +71,49 @@ describe("searchAdminUsers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("appointAdmin", () => {
|
||||
it("기존 custom claims를 보존하면서 관리자 권한을 부여하고 재호출은 멱등이다", async () => {
|
||||
describe("setAdminRole", () => {
|
||||
it("기존 custom claims를 보존하면서 관리자 권한을 부여·해제하고 재호출은 멱등이다", async () => {
|
||||
const uid = "admin-user-search-test-1";
|
||||
await auth.setCustomUserClaims(uid, { betaTester: true });
|
||||
|
||||
await expect(appointAdmin(uid, "granting-admin")).resolves.toEqual({
|
||||
await expect(setAdminRole(uid, true, "granting-admin")).resolves.toEqual({
|
||||
uid,
|
||||
isAdmin: true,
|
||||
alreadyAdmin: false,
|
||||
changed: true,
|
||||
});
|
||||
const record = await auth.getUser(uid);
|
||||
expect(record.customClaims).toMatchObject({
|
||||
betaTester: true,
|
||||
admin: true,
|
||||
});
|
||||
await expect(appointAdmin(uid, "granting-admin")).resolves.toMatchObject({
|
||||
await expect(setAdminRole(uid, true, "granting-admin")).resolves.toMatchObject({
|
||||
isAdmin: true,
|
||||
alreadyAdmin: true,
|
||||
changed: false,
|
||||
});
|
||||
const [dto] = await searchAdminUsers(uid, 10);
|
||||
expect(dto.isAdmin).toBe(true);
|
||||
|
||||
await expect(setAdminRole(uid, false, "other-admin")).resolves.toEqual({
|
||||
uid,
|
||||
isAdmin: false,
|
||||
changed: true,
|
||||
});
|
||||
const revoked = await auth.getUser(uid);
|
||||
expect(revoked.customClaims).toMatchObject({ betaTester: true });
|
||||
expect(revoked.customClaims).not.toHaveProperty("admin");
|
||||
});
|
||||
|
||||
it("비활성 유저는 관리자로 임명할 수 없다", async () => {
|
||||
await expect(
|
||||
appointAdmin("admin-user-search-test-2", "granting-admin")
|
||||
setAdminRole("admin-user-search-test-2", true, "granting-admin")
|
||||
).rejects.toMatchObject({ status: 409, code: "INACTIVE_USER" });
|
||||
});
|
||||
|
||||
it("현재 관리자는 자신의 권한을 해제할 수 없다", async () => {
|
||||
await expect(
|
||||
setAdminRole("admin-user-search-test-1", false, "admin-user-search-test-1")
|
||||
).rejects.toMatchObject({ status: 409, code: "CANNOT_REMOVE_SELF_ADMIN" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("listAdminUsers", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user