This commit is contained in:
Your Name committed 2026-10-01 17:26:18 +07:00
1 parent 78b722e120
commit 75770ca122
18 files changed
+1119 -318

No files matched your search

@@ -0,0 +1,36 @@
import { describe, expect, test } from 'bun:test'
import { settingsRoute } from './settings'
function jsonRequest(path: string, method: string, body: unknown) {
return new Request(`http://localhost${path}`, {
method,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
}
describe('footer layout route boundaries', () => {
for (const method of ['PUT', 'POST']) {
test(`${method} /settings rejects footerLayout writes`, async () => {
const response = await settingsRoute.handle(jsonRequest('/settings', method, {
footerLayout: { columns: ['brand'] },
}))
expect(response.status).toBe(400)
expect(await response.json()).toEqual({
error: 'Use PATCH /settings/footer-layout to update footer layout',
})
})
}
test('PATCH /settings/footer-layout requires admin authentication', async () => {
const response = await settingsRoute.handle(jsonRequest('/settings/footer-layout', 'PATCH', {
dryRun: true,
}))
expect(response.status).toBe(401)
expect(await response.json()).toEqual({
error: 'Unauthorized: Missing or invalid token format',
})
})
})
+109 -4
View File
@@ -1,6 +1,25 @@
import { Elysia, t } from "elysia";
import { FooterLayoutSettingsPatchSchema } from "@homaroma/types";
import { z } from "zod";
import prisma from "../lib/prisma";
import { adminOnly } from "../middleware/auth";
import { adminOnly, requireAdmin } from "../middleware/auth";
import {
mergeFooterLayoutSettings,
normalizeFooterLayoutSettings,
} from "../lib/footerLayoutSettings";
const FooterLayoutUpdateRequestSchema = FooterLayoutSettingsPatchSchema.extend({
expectedVersion: z.string().optional(),
dryRun: z.boolean().optional(),
}).strict().superRefine((value, context) => {
if (!value.dryRun && !value.expectedVersion) {
context.addIssue({
code: "custom",
path: ["expectedVersion"],
message: "expectedVersion is required when updating footer layout",
});
}
});
export async function saveStoreSettingsTranslations(settingsId: number, payload: any) {
const results: any[] = [];
@@ -171,6 +190,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
const mobileHeaderStyle = (settings.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center';
return {
...settingsData,
footerLayout: normalizeFooterLayoutSettings((settingsData as any).footerLayout),
translations,
headerBehavior,
mobileHeaderBehavior,
@@ -220,6 +240,21 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
return { error: "Failed to fetch footer brand" };
}
})
.get("/footer-layout", async ({ set }) => {
try {
const settings = await prisma.storeSettings.findFirst();
const current = settings ?? await prisma.storeSettings.create({ data: {} });
return {
version: current.updatedAt.toISOString(),
layout: normalizeFooterLayoutSettings((current as any).footerLayout),
};
} catch (err) {
console.error("[Settings] Footer layout fetch error:", err);
set.status = 500;
return { error: "Failed to fetch footer layout" };
}
})
.use(adminOnly)
.patch("/footer-brand", async ({ body, set }: { body: any; set: any }) => {
try {
@@ -271,10 +306,74 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
set.status = 400;
return { error: err?.message || "Failed to update footer brand" };
}
}, { body: t.Any() })
}, { beforeHandle: requireAdmin, body: t.Any() })
.patch("/footer-layout", async ({ body, set }: { body: unknown; set: any }) => {
const request = FooterLayoutUpdateRequestSchema.safeParse(body);
if (!request.success) {
set.status = 400;
return { error: "Invalid footer layout", issues: request.error.issues };
}
try {
const { expectedVersion, dryRun, ...patch } = request.data;
const settings = await prisma.storeSettings.findFirst();
const current = settings ?? await prisma.storeSettings.create({ data: {} });
const version = current.updatedAt.toISOString();
if (expectedVersion && expectedVersion !== version) {
set.status = 409;
return {
error: "Footer layout has changed since it was read",
code: "VERSION_CONFLICT",
expectedVersion,
currentVersion: version,
};
}
const currentLayout = normalizeFooterLayoutSettings((current as any).footerLayout);
const layout = mergeFooterLayoutSettings(currentLayout, patch);
if (dryRun) {
return { dryRun: true, version, wouldUpdate: layout };
}
const updated = await prisma.$transaction(async (transaction) => {
const result = await transaction.storeSettings.updateMany({
where: { id: current.id, updatedAt: current.updatedAt },
data: { footerLayout: layout as any },
});
if (result.count === 0) return null;
return transaction.storeSettings.findUniqueOrThrow({ where: { id: current.id } });
});
if (!updated) {
const latest = await prisma.storeSettings.findUnique({ where: { id: current.id } });
set.status = 409;
return {
error: "Footer layout has changed since it was read",
code: "VERSION_CONFLICT",
expectedVersion,
currentVersion: latest?.updatedAt.toISOString() ?? version,
};
}
return {
success: true,
version: updated.updatedAt.toISOString(),
layout,
};
} catch (err) {
console.error("[Settings] Footer layout update error:", err);
set.status = 500;
return { error: "Failed to update footer layout" };
}
}, { beforeHandle: requireAdmin, body: t.Any() })
.put("/", async ({ body, set }: { body: any; set: any }) => {
try {
const { headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any;
const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any;
if (footerLayout !== undefined) {
set.status = 400;
return { error: "Use PATCH /settings/footer-layout to update footer layout" };
}
const currentTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
? data.themeTokens
: {};
@@ -354,12 +453,17 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
defaultOgImage: t.Optional(t.Nullable(t.String())),
brandPrompt: t.Optional(t.Nullable(t.String())),
footerLayout: t.Optional(t.Any()),
translations: t.Optional(t.Any()),
})
})
.post("/", async ({ body, set }: { body: any; set: any }) => {
try {
const { headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any;
const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any;
if (footerLayout !== undefined) {
set.status = 400;
return { error: "Use PATCH /settings/footer-layout to update footer layout" };
}
const currentTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
? data.themeTokens
: {};
@@ -439,6 +543,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
defaultOgImage: t.Optional(t.Nullable(t.String())),
brandPrompt: t.Optional(t.Nullable(t.String())),
footerLayout: t.Optional(t.Any()),
translations: t.Optional(t.Any()),
})
})