fix(security): authorize site uploads and harden tokens, media and request identity
This commit is contained in:
1 parent
52f6d1491f
commit
8abfe352ef
70 files changed
+1609
-204
No files matched your search
@@ -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");
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { CheckCircle2, Clock, MailX } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { isValidVerificationToken } from "@/actions/email-verify";
|
||||
import Link from "@/components/link";
|
||||
import { SurfaceCard } from "@/components/surface-card";
|
||||
import { isValidVerificationToken } from "@/lib/auth/email-verification";
|
||||
import { db, User } from "@/lib/db";
|
||||
|
||||
type Status = "verified" | "already" | "invalid" | "unavailable";
|
||||
|
||||
Reference in new issue
Block a user