Harden CMS security and theme contrast

This commit is contained in:
Simo committed 2026-07-11 20:27:20 +02:00
1 parent 2465ff2170
commit 4a1e1115b3
57 files changed
+1023 -231

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import {
authorizeTopupCapture,
claimTopupDelivery,
recordCreatedTopup,
} from "@/lib/services/paypal-topup";
describe("authorizeTopupCapture", () => {
it("rejects an order created by a different user", async () => {
const findOrder = async () => ({
userId: 41,
status: "CREATED",
});
await expect(authorizeTopupCapture(99, "ORDER-123", findOrder)).rejects.toMatchObject({
code: "ORDER_NOT_FOUND",
});
});
it("rejects an order that has already left the created state", async () => {
const findOrder = async () => ({
userId: 99,
status: "COMPLETED",
});
await expect(authorizeTopupCapture(99, "ORDER-123", findOrder)).rejects.toMatchObject({
code: "ORDER_ALREADY_PROCESSED",
});
});
it("resumes credit delivery for a captured order without capturing it again", async () => {
const findOrder = async () => ({
userId: 99,
status: "CAPTURED_PENDING_CREDIT",
amount: 12.5,
});
await expect(authorizeTopupCapture(99, "ORDER-123", findOrder)).resolves.toMatchObject({
action: "DELIVER_CREDITS",
amount: 12.5,
});
});
});
describe("recordCreatedTopup", () => {
it("stores the PayPal order id with its owner before approval", async () => {
let saved: Parameters<typeof record>[0] | undefined;
const record = async (data: {
userId: number;
transactionId: string;
status: string;
description: string;
amount: number;
currency: string;
createdAt: Date;
updatedAt: Date;
}) => {
saved = data;
};
await recordCreatedTopup(
{ userId: 99, orderId: "ORDER-123", amount: 12.5, currency: "EUR", credits: 1250 },
record,
);
expect(saved).toMatchObject({
userId: 99,
transactionId: "ORDER-123",
status: "CREATED",
amount: 12.5,
currency: "EUR",
});
});
});
describe("claimTopupDelivery", () => {
it("rejects delivery when another request already claimed the order", async () => {
const claim = async () => ({ count: 0 });
await expect(claimTopupDelivery(claim)).rejects.toMatchObject({
code: "DELIVERY_ALREADY_CLAIMED",
});
});
it("allows delivery after atomically claiming the pending order", async () => {
const claim = async () => ({ count: 1 });
await expect(claimTopupDelivery(claim)).resolves.toBeUndefined();
});
});
+84
View File
@@ -0,0 +1,84 @@
export interface PendingTopup {
userId: number;
status: string | null;
amount?: number;
}
export type AuthorizedTopup = PendingTopup & { action: "CAPTURE" | "DELIVER_CREDITS" };
export type FindTopupOrder = (orderId: string) => Promise<PendingTopup | null>;
export interface CreatedTopupRecord {
userId: number;
transactionId: string;
status: "CREATED";
description: string;
amount: number;
currency: string;
createdAt: Date;
updatedAt: Date;
}
export type CreateTopupRecord = (data: CreatedTopupRecord) => Promise<unknown>;
export class TopupCaptureError extends Error {
constructor(public readonly code: "ORDER_NOT_FOUND" | "ORDER_ALREADY_PROCESSED") {
super(code);
}
}
export class TopupDeliveryError extends Error {
readonly code = "DELIVERY_ALREADY_CLAIMED";
constructor() {
super("DELIVERY_ALREADY_CLAIMED");
}
}
export async function claimTopupDelivery(
claim: () => Promise<{ count: number }>,
): Promise<void> {
const result = await claim();
if (result.count !== 1) throw new TopupDeliveryError();
}
export async function authorizeTopupCapture(
userId: number,
orderId: string,
findOrder: FindTopupOrder,
): Promise<AuthorizedTopup> {
const order = await findOrder(orderId);
if (!order || order.userId !== userId) {
throw new TopupCaptureError("ORDER_NOT_FOUND");
}
if (order.status === "CAPTURED_PENDING_CREDIT" && Number.isFinite(order.amount)) {
return { ...order, action: "DELIVER_CREDITS" };
}
if (order.status !== "CREATED") {
throw new TopupCaptureError("ORDER_ALREADY_PROCESSED");
}
return { ...order, action: "CAPTURE" };
}
export async function recordCreatedTopup(
topup: {
userId: number;
orderId: string;
amount: number;
currency: string;
credits: number;
},
createRecord: CreateTopupRecord,
): Promise<void> {
const now = new Date();
await createRecord({
userId: topup.userId,
transactionId: topup.orderId,
status: "CREATED",
description: `Top-up: ${topup.credits} credits`,
amount: topup.amount,
currency: topup.currency,
createdAt: now,
updatedAt: now,
});
}
+46
View File
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { createOwnedTicketReply } from "@/lib/services/ticket-replies";
describe("createOwnedTicketReply", () => {
it("does not create a reply for a ticket owned by another user", async () => {
let created = false;
const db = {
findTicket: async () => ({ id: 5n, userId: 7 }),
createReply: async () => {
created = true;
throw new Error("must not run");
},
touchTicket: async () => undefined,
};
await expect(
createOwnedTicketReply(db, { ticketId: 5n, userId: 99, content: "Help" }),
).resolves.toBeNull();
expect(created).toBe(false);
});
it("creates the reply and updates ticket activity as one operation", async () => {
let touchedAt: Date | undefined;
const db = {
findTicket: async () => ({ id: 5n, userId: 99 }),
createReply: async (data: { createdAt: Date }) => ({
id: 8n,
userId: 99,
content: "Help",
createdAt: data.createdAt,
}),
touchTicket: async (_ticketId: bigint, updatedAt: Date) => {
touchedAt = updatedAt;
},
};
const reply = await createOwnedTicketReply(db, {
ticketId: 5n,
userId: 99,
content: "Help",
});
expect(reply).toMatchObject({ id: 8n, userId: 99, content: "Help" });
expect(touchedAt).toBe(reply?.createdAt);
});
});
+31
View File
@@ -0,0 +1,31 @@
export interface TicketReplyRecord {
id: bigint;
userId: number;
content: string;
createdAt: Date | null;
}
export interface TicketReplyDb {
findTicket(ticketId: bigint): Promise<{ id: bigint; userId: number | null } | null>;
createReply(data: {
ticketId: bigint;
userId: number;
content: string;
createdAt: Date;
updatedAt: Date;
}): Promise<TicketReplyRecord>;
touchTicket(ticketId: bigint, updatedAt: Date): Promise<unknown>;
}
export async function createOwnedTicketReply(
db: TicketReplyDb,
input: { ticketId: bigint; userId: number; content: string },
): Promise<TicketReplyRecord | null> {
const ticket = await db.findTicket(input.ticketId);
if (!ticket || ticket.userId !== input.userId) return null;
const now = new Date();
const reply = await db.createReply({ ...input, createdAt: now, updatedAt: now });
await db.touchTicket(input.ticketId, now);
return reply;
}