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 type { OrderStatus, ProductDoc } from "../types/reward";
|
||||||
import { transitionOrder, updateOrderShipment } from "../services/orderService";
|
import { transitionOrder, updateOrderShipment } from "../services/orderService";
|
||||||
import { invalidateRewardCatalog } from "../services/rewardCatalogService";
|
import { invalidateRewardCatalog } from "../services/rewardCatalogService";
|
||||||
import { appointAdmin, listAdminUsers, searchAdminUsers } from "../services/adminUserService";
|
import { listAdminUsers, searchAdminUsers, setAdminRole } from "../services/adminUserService";
|
||||||
import {
|
import {
|
||||||
toAdminProductDto, toLedgerEntryDto, toOrderDto, toWalletDto,
|
toAdminProductDto, toLedgerEntryDto, toOrderDto, toWalletDto,
|
||||||
type AdminProductDto, type LedgerPageDto, type OrderPageDto,
|
type AdminProductDto, type LedgerPageDto, type OrderPageDto,
|
||||||
@ -133,7 +133,7 @@ export const admin = onRequest(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tail === "users/admin" && req.method === "POST") {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,13 +27,14 @@ async function attachAdminStatus(users: AdminUserDto[]): Promise<AdminUserDto[]>
|
|||||||
|
|
||||||
export interface AdminRoleUpdateResult {
|
export interface AdminRoleUpdateResult {
|
||||||
uid: string;
|
uid: string;
|
||||||
isAdmin: true;
|
isAdmin: boolean;
|
||||||
alreadyAdmin: boolean;
|
changed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 기존 custom claims를 보존하면서 활성 유저에게 관리자 권한을 부여한다. */
|
/** 기존 custom claims를 보존하면서 관리자 권한을 부여하거나 해제한다. */
|
||||||
export async function appointAdmin(
|
export async function setAdminRole(
|
||||||
uidRaw: unknown,
|
uidRaw: unknown,
|
||||||
|
isAdminRaw: unknown,
|
||||||
actorUid: string
|
actorUid: string
|
||||||
): Promise<AdminRoleUpdateResult> {
|
): Promise<AdminRoleUpdateResult> {
|
||||||
if (
|
if (
|
||||||
@ -45,23 +46,30 @@ export async function appointAdmin(
|
|||||||
) {
|
) {
|
||||||
throw new HttpError(400, "invalid uid", "INVALID_INPUT");
|
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);
|
const user = await getUser(uidRaw);
|
||||||
if (!user) throw new HttpError(404, "user not found", "USER_NOT_FOUND");
|
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");
|
throw new HttpError(409, "inactive user cannot be admin", "INACTIVE_USER");
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await auth.getUser(uidRaw).catch(() => {
|
const record = await auth.getUser(uidRaw).catch(() => {
|
||||||
throw new HttpError(404, "auth user not found", "USER_NOT_FOUND");
|
throw new HttpError(404, "auth user not found", "USER_NOT_FOUND");
|
||||||
});
|
});
|
||||||
if (record.customClaims?.admin === true) {
|
const currentIsAdmin = record.customClaims?.admin === true;
|
||||||
return { uid: uidRaw, isAdmin: true, alreadyAdmin: true };
|
if (currentIsAdmin === isAdminRaw) {
|
||||||
|
return { uid: uidRaw, isAdmin: isAdminRaw, changed: false };
|
||||||
}
|
}
|
||||||
await auth.setCustomUserClaims(uidRaw, {
|
const nextClaims: Record<string, unknown> = { ...(record.customClaims ?? {}) };
|
||||||
...(record.customClaims ?? {}),
|
if (isAdminRaw) nextClaims.admin = true;
|
||||||
admin: true,
|
else delete nextClaims.admin;
|
||||||
});
|
await auth.setCustomUserClaims(uidRaw, nextClaims);
|
||||||
return { uid: uidRaw, isAdmin: true, alreadyAdmin: false };
|
return { uid: uidRaw, isAdmin: isAdminRaw, changed: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { beforeAll, describe, expect, it } from "vitest";
|
import { beforeAll, describe, expect, it } from "vitest";
|
||||||
import { Timestamp } from "firebase-admin/firestore";
|
import { Timestamp } from "firebase-admin/firestore";
|
||||||
import { auth, firestore } from "../../src/firebase";
|
import { auth, firestore } from "../../src/firebase";
|
||||||
import { appointAdmin, listAdminUsers, searchAdminUsers } from "../../src/services/adminUserService";
|
import { listAdminUsers, searchAdminUsers, setAdminRole } from "../../src/services/adminUserService";
|
||||||
|
|
||||||
// 다른 테스트가 만드는 users 문서와 섞이지 않도록 고유 접두어를 쓴다.
|
// 다른 테스트가 만드는 users 문서와 섞이지 않도록 고유 접두어를 쓴다.
|
||||||
const PREFIX = "검색전용";
|
const PREFIX = "검색전용";
|
||||||
@ -71,34 +71,49 @@ describe("searchAdminUsers", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("appointAdmin", () => {
|
describe("setAdminRole", () => {
|
||||||
it("기존 custom claims를 보존하면서 관리자 권한을 부여하고 재호출은 멱등이다", async () => {
|
it("기존 custom claims를 보존하면서 관리자 권한을 부여·해제하고 재호출은 멱등이다", async () => {
|
||||||
const uid = "admin-user-search-test-1";
|
const uid = "admin-user-search-test-1";
|
||||||
await auth.setCustomUserClaims(uid, { betaTester: true });
|
await auth.setCustomUserClaims(uid, { betaTester: true });
|
||||||
|
|
||||||
await expect(appointAdmin(uid, "granting-admin")).resolves.toEqual({
|
await expect(setAdminRole(uid, true, "granting-admin")).resolves.toEqual({
|
||||||
uid,
|
uid,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
alreadyAdmin: false,
|
changed: true,
|
||||||
});
|
});
|
||||||
const record = await auth.getUser(uid);
|
const record = await auth.getUser(uid);
|
||||||
expect(record.customClaims).toMatchObject({
|
expect(record.customClaims).toMatchObject({
|
||||||
betaTester: true,
|
betaTester: true,
|
||||||
admin: true,
|
admin: true,
|
||||||
});
|
});
|
||||||
await expect(appointAdmin(uid, "granting-admin")).resolves.toMatchObject({
|
await expect(setAdminRole(uid, true, "granting-admin")).resolves.toMatchObject({
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
alreadyAdmin: true,
|
changed: false,
|
||||||
});
|
});
|
||||||
const [dto] = await searchAdminUsers(uid, 10);
|
const [dto] = await searchAdminUsers(uid, 10);
|
||||||
expect(dto.isAdmin).toBe(true);
|
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 () => {
|
it("비활성 유저는 관리자로 임명할 수 없다", async () => {
|
||||||
await expect(
|
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" });
|
).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", () => {
|
describe("listAdminUsers", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user