fix(security): authorize site uploads and harden tokens, media and request identity
CI / check (push) Successful in 3m15s
CI / deploy (push) Successful in 1m19s
CI / publish-container (push) Successful in 48s

This commit is contained in:
Simo committed 2026-09-13 19:24:43 +02:00
1 parent 52f6d1491f
commit 8abfe352ef
70 files changed
+1609 -204

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import { renderToStaticMarkup } from "react-dom/server";
import { beforeEach, expect, it, vi } from "vitest";
import LogoGenerator from "@/components/public/logo-generator";
import { PERMS } from "@/lib/permission-slugs";
import LogoPage from "./page";
const state = vi.hoisted(() => ({ context: null as unknown }));
vi.mock("@/actions/save-logo", () => ({ saveLogo: vi.fn() }));
vi.mock("@/lib/hotel-name", () => ({
resolveHotelName: async () => "Fixture hotel",
}));
vi.mock("@/lib/permissions", async () => ({
...(await import("@/lib/permission-slugs")),
getApiAdminContext: async () => state.context,
canAccess: (permissions: { has: (slug: string) => boolean }, slug: string) =>
permissions.has(slug),
}));
beforeEach(() => {
state.context = null;
});
it.each([
["anonymous", null, false],
["settings viewer", [PERMS.ADMIN_DASHBOARD, PERMS.SETTINGS_VIEW], false],
["editor without housekeeping access", [PERMS.SETTINGS_EDIT], false],
["settings editor", [PERMS.ADMIN_DASHBOARD, PERMS.SETTINGS_EDIT], true],
] as const)(
"offers site-logo saving only to authorized %s",
async (_name, slugs, canSave) => {
state.context = slugs
? {
session: { user: { rank: 7 } },
permissions: {
has: (slug: string) => (slugs as readonly string[]).includes(slug),
},
}
: null;
const html = renderToStaticMarkup(await LogoPage());
expect(html.includes("Save as site logo")).toBe(canSave);
expect(html).toContain("Download PNG");
expect(html).toContain("Download all fonts");
},
);
it("defaults the generator to download-only without server authorization", () => {
const html = renderToStaticMarkup(<LogoGenerator />);
expect(html).not.toContain("Save as site logo");
expect(html).toContain("Download PNG");
});
+20 -3
View File
@@ -1,6 +1,7 @@
import LogoGenerator from "@/components/public/logo-generator";
import { ContentCard } from "@/components/public/ui";
import { resolveHotelName } from "@/lib/hotel-name";
import { canAccess, getApiAdminContext, PERMS } from "@/lib/permissions";
export const metadata = { title: "Logo generator" };
@@ -8,10 +9,26 @@ export const metadata = { title: "Logo generator" };
* Public logo generator (AtomCMS logo-generator.blade). Server wrapper that
* renders the atom-styled ContentCard header and hands off to the fully
* client-side <LogoGenerator>, which does all styling, live preview, and PNG
* export in the browser (no server data, no DB).
* export in the browser. Saving the site logo requires settings edit access.
*/
export default async function LogoPage() {
const initialText = await resolveHotelName();
const [initialText, context] = await Promise.all([
resolveHotelName(),
getApiAdminContext(),
]);
const canSaveToSite = Boolean(
context &&
canAccess(
context.permissions,
PERMS.ADMIN_DASHBOARD,
context.session.user.rank,
) &&
canAccess(
context.permissions,
PERMS.SETTINGS_EDIT,
context.session.user.rank,
),
);
return (
<main
style={{
@@ -27,7 +44,7 @@ export default async function LogoPage() {
subtitle="Design a logo for your hotel — pick a font, colours and size, then download it as a PNG."
/>
<LogoGenerator initialText={initialText} />
<LogoGenerator initialText={initialText} canSaveToSite={canSaveToSite} />
</main>
);
}