fix(housekeeping): harden people account workflows
This commit is contained in:
1 parent
e1b31ff773
commit
25b76437ff
41 files changed
+2657
-838
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
||||
@@ -18,11 +18,7 @@ export async function dismissApplication(formData: FormData): Promise<void> {
|
||||
if (!Number.isSafeInteger(applicationId) || applicationId <= 0) return;
|
||||
|
||||
await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.USERS_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"application.decide",
|
||||
{ applicationId, decision: "dismiss" },
|
||||
);
|
||||
|
||||
@@ -5,13 +5,11 @@ import { disbandGuild } from "./admin-guilds";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(staff, permission, correlationId) => ({
|
||||
staff,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermissionRateLimited: vi.fn() }));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
||||
@@ -16,11 +16,7 @@ export async function disbandGuild(formData: FormData): Promise<void> {
|
||||
if (!Number.isSafeInteger(guildId) || guildId <= 0) return;
|
||||
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.USERS_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"guild.disband",
|
||||
{ guildId },
|
||||
);
|
||||
|
||||
@@ -10,13 +10,11 @@ import {
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(staff, permission, correlationId) => ({
|
||||
staff,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
||||
@@ -37,11 +37,7 @@ async function run(
|
||||
const id = "id" in input ? input.id : undefined;
|
||||
if (!adding && !(Number.isSafeInteger(id) && Number(id) > 0)) return;
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.SETTINGS_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"ip.action",
|
||||
input,
|
||||
);
|
||||
|
||||
@@ -5,13 +5,11 @@ import { createTeam, deleteTeam } from "./admin-teams";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(staff, permission, correlationId) => ({
|
||||
staff,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
||||
@@ -20,11 +20,7 @@ export async function createTeam(formData: FormData): Promise<void> {
|
||||
const rankName = text(formData, "rankName");
|
||||
if (!rankName) return;
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.USERS_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"team.change",
|
||||
{
|
||||
action: "create",
|
||||
@@ -44,11 +40,7 @@ export async function deleteTeam(formData: FormData): Promise<void> {
|
||||
const teamId = Number(formData.get("id"));
|
||||
if (!Number.isSafeInteger(teamId) || teamId <= 0) return;
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.USERS_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"team.change",
|
||||
{ action: "delete", teamId },
|
||||
);
|
||||
|
||||
@@ -6,13 +6,11 @@ import { saveVpn } from "./admin-vpn";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(staff, permission, correlationId) => ({
|
||||
staff,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
||||
@@ -20,11 +20,7 @@ export async function saveVpn(formData: FormData): Promise<void> {
|
||||
.toLowerCase();
|
||||
const provider = ALLOWED_PROVIDERS.has(rawProvider) ? rawProvider : "none";
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.SETTINGS_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"vpn.configure",
|
||||
{
|
||||
enabled: String(formData.get("vpn_block_enabled") ?? "").trim() !== "",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
||||
@@ -24,11 +24,7 @@ export async function addWord(input: {
|
||||
.slice(0, 255);
|
||||
if (!word) return actionError("Word is required");
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.WORDFILTER_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"word-filter.update",
|
||||
{ action: "add", word },
|
||||
);
|
||||
@@ -44,11 +40,7 @@ export async function deleteWord(input: { id: string }): Promise<ActionResult> {
|
||||
if (!Number.isSafeInteger(id) || id <= 0)
|
||||
return actionError("Missing word id");
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.WORDFILTER_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
"word-filter.update",
|
||||
{ action: "delete", id },
|
||||
);
|
||||
|
||||
@@ -3,13 +3,11 @@ import { requirePermission } from "@/lib/admin/guard";
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(staff, permission, correlationId) => ({
|
||||
staff,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
|
||||
@@ -10,13 +10,11 @@ import {
|
||||
|
||||
const { execute } = vi.hoisted(() => ({ execute: vi.fn() }));
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(staff, permission, correlationId) => ({
|
||||
staff,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
@@ -88,7 +86,7 @@ describe("legacy bulk user wrappers", () => {
|
||||
data: { userId: 9, untilUnix: 1234 },
|
||||
});
|
||||
expect(execute).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ permission: "admin.users.edit" }),
|
||||
expect.objectContaining({ expectedActorId: 1 }),
|
||||
"user.trade-lock",
|
||||
{ userId: 9, untilUnix: 1234 },
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
|
||||
@@ -27,11 +27,7 @@ async function executeLegacy(
|
||||
input: unknown,
|
||||
) {
|
||||
return peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
staff,
|
||||
PERMS.USERS_EDIT,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(staff, createCorrelationId()),
|
||||
operation,
|
||||
input,
|
||||
);
|
||||
@@ -259,6 +255,14 @@ export async function setTradeLock({
|
||||
userId,
|
||||
untilUnix: until,
|
||||
});
|
||||
if (!result.ok) return { ok: false, error: "Trade lock update failed" };
|
||||
if (!result.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
result.error.code === "NOT_FOUND"
|
||||
? "User not found"
|
||||
: "Trade lock update failed",
|
||||
};
|
||||
}
|
||||
return { ok: true, data: { userId, untilUnix: until } };
|
||||
}
|
||||
@@ -8,13 +8,11 @@ const { execute, staff } = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(actor, permission, correlationId) => ({
|
||||
actor,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
|
||||
@@ -98,12 +96,13 @@ describe("legacy user safe-action wrappers", () => {
|
||||
}>
|
||||
)({ userId: 7 });
|
||||
|
||||
expect(
|
||||
execute.mock.calls.map((call) => [call[0].permission, call[1]]),
|
||||
).toEqual([
|
||||
["admin.users.edit", "user.update"],
|
||||
["admin.users.ban", "user.ban"],
|
||||
["admin.users.reset_password", "user.reset-password"],
|
||||
expect(execute.mock.calls.map((call) => call[1])).toEqual([
|
||||
"user.update",
|
||||
"user.ban",
|
||||
"user.reset-password",
|
||||
]);
|
||||
expect(execute.mock.calls.map((call) => call[0].expectedActorId)).toEqual([
|
||||
1, 1, 1,
|
||||
]);
|
||||
expect(reset.data.newPassword).toBe("temporary-password");
|
||||
});
|
||||
@@ -113,7 +112,7 @@ describe("legacy application and word-filter wrappers", () => {
|
||||
it("keeps tolerant application dismissal and /admin revalidation", async () => {
|
||||
await dismissApplication(form({ id: "9" }));
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ permission: "admin.users.edit" }),
|
||||
expect.objectContaining({ expectedActorId: 1 }),
|
||||
"application.decide",
|
||||
{ applicationId: 9, decision: "dismiss" },
|
||||
);
|
||||
|
||||
@@ -7,13 +7,11 @@ const { execute, staff } = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
|
||||
createLegacyPeopleMutationContext: vi.fn(
|
||||
(actor, permission, correlationId) => ({
|
||||
actor,
|
||||
permission,
|
||||
correlationId,
|
||||
}),
|
||||
),
|
||||
createPeopleMutationInvocation: vi.fn((staff, correlationId) => ({
|
||||
expectedActorId: staff.id,
|
||||
correlationId,
|
||||
legacy: true,
|
||||
})),
|
||||
peopleMutationService: { execute },
|
||||
}));
|
||||
vi.mock("@/lib/admin/guard", () => ({
|
||||
|
||||
+11
-21
@@ -3,7 +3,7 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
createLegacyPeopleMutationContext,
|
||||
createPeopleMutationInvocation,
|
||||
type PeopleMutationOperation,
|
||||
peopleMutationService,
|
||||
} from "@/features/housekeeping/domains/people/services/mutations";
|
||||
@@ -116,16 +116,11 @@ const legacyMessages: Partial<Record<PeopleMutationOperation, string>> = {
|
||||
|
||||
async function executeLegacy(
|
||||
ctx: { session: { user: { id: number; username: string; rank: number } } },
|
||||
permission: string,
|
||||
operation: PeopleMutationOperation,
|
||||
input: unknown,
|
||||
) {
|
||||
const result = await peopleMutationService.execute(
|
||||
createLegacyPeopleMutationContext(
|
||||
ctx.session.user,
|
||||
permission,
|
||||
createCorrelationId(),
|
||||
),
|
||||
createPeopleMutationInvocation(ctx.session.user, createCorrelationId()),
|
||||
operation,
|
||||
input,
|
||||
);
|
||||
@@ -148,7 +143,7 @@ export const updateUser = adminAction(
|
||||
{ permission: PERMS.USERS_EDIT, schema: updateUserInput },
|
||||
async (ctx) => {
|
||||
const { id: userId, ...fields } = ctx.data;
|
||||
await executeLegacy(ctx, PERMS.USERS_EDIT, "user.update", {
|
||||
await executeLegacy(ctx, "user.update", {
|
||||
userId,
|
||||
fields,
|
||||
});
|
||||
@@ -159,7 +154,7 @@ export const updateUser = adminAction(
|
||||
export const banUser = adminAction(
|
||||
{ permission: PERMS.USERS_BAN, schema: banUserSchema },
|
||||
async (ctx) => {
|
||||
await executeLegacy(ctx, PERMS.USERS_BAN, "user.ban", ctx.data);
|
||||
await executeLegacy(ctx, "user.ban", ctx.data);
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
@@ -169,7 +164,7 @@ const userIdSchema = z.object({ userId: z.coerce.number().int().positive() });
|
||||
export const unbanUser = adminAction(
|
||||
{ permission: PERMS.USERS_BAN, schema: userIdSchema },
|
||||
async (ctx) => {
|
||||
await executeLegacy(ctx, PERMS.USERS_BAN, "user.unban", ctx.data);
|
||||
await executeLegacy(ctx, "user.unban", ctx.data);
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
@@ -177,12 +172,7 @@ export const unbanUser = adminAction(
|
||||
export const resetPassword = adminAction(
|
||||
{ permission: PERMS.USERS_RESET_PASSWORD, schema: userIdSchema },
|
||||
async (ctx) => {
|
||||
const snapshot = await executeLegacy(
|
||||
ctx,
|
||||
PERMS.USERS_RESET_PASSWORD,
|
||||
"user.reset-password",
|
||||
ctx.data,
|
||||
);
|
||||
const snapshot = await executeLegacy(ctx, "user.reset-password", ctx.data);
|
||||
return actionOk({
|
||||
newPassword: String(snapshot.output?.newPassword ?? ""),
|
||||
});
|
||||
@@ -192,7 +182,7 @@ export const resetPassword = adminAction(
|
||||
export const disconnectUser = adminAction(
|
||||
{ permission: PERMS.USERS_EDIT, schema: userIdSchema },
|
||||
async (ctx) => {
|
||||
await executeLegacy(ctx, PERMS.USERS_EDIT, "user.disconnect", ctx.data);
|
||||
await executeLegacy(ctx, "user.disconnect", ctx.data);
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
@@ -203,7 +193,7 @@ const alertUserSchema = userIdSchema.extend({
|
||||
export const alertUser = adminAction(
|
||||
{ permission: PERMS.USERS_EDIT, schema: alertUserSchema },
|
||||
async (ctx) => {
|
||||
await executeLegacy(ctx, PERMS.USERS_EDIT, "user.alert", ctx.data);
|
||||
await executeLegacy(ctx, "user.alert", ctx.data);
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
@@ -214,7 +204,7 @@ const muteSchema = userIdSchema.extend({
|
||||
export const muteUser = adminAction(
|
||||
{ permission: PERMS.USERS_EDIT, schema: muteSchema },
|
||||
async (ctx) => {
|
||||
await executeLegacy(ctx, PERMS.USERS_EDIT, "user.mute", ctx.data);
|
||||
await executeLegacy(ctx, "user.mute", ctx.data);
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
@@ -222,7 +212,7 @@ export const muteUser = adminAction(
|
||||
export const unmuteUser = adminAction(
|
||||
{ permission: PERMS.USERS_EDIT, schema: userIdSchema },
|
||||
async (ctx) => {
|
||||
await executeLegacy(ctx, PERMS.USERS_EDIT, "user.unmute", ctx.data);
|
||||
await executeLegacy(ctx, "user.unmute", ctx.data);
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
@@ -233,7 +223,7 @@ const sendCreditsSchema = userIdSchema.extend({
|
||||
export const sendCredits = adminAction(
|
||||
{ permission: PERMS.USERS_EDIT, schema: sendCreditsSchema },
|
||||
async (ctx) => {
|
||||
await executeLegacy(ctx, PERMS.USERS_EDIT, "user.send-currency", ctx.data);
|
||||
await executeLegacy(ctx, "user.send-currency", ctx.data);
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user