From fcc8646ee40aaaaf80b9aec6155aa046dcbe5d34 Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Sun, 16 Nov 2025 10:52:11 +0100 Subject: [PATCH 1/9] Move door loading from page to layout --- .../(app)/admin/doors/{+page.server.ts => +layout.server.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/routes/(app)/admin/doors/{+page.server.ts => +layout.server.ts} (69%) diff --git a/src/routes/(app)/admin/doors/+page.server.ts b/src/routes/(app)/admin/doors/+layout.server.ts similarity index 69% rename from src/routes/(app)/admin/doors/+page.server.ts rename to src/routes/(app)/admin/doors/+layout.server.ts index 5b6457849..140fdaa65 100644 --- a/src/routes/(app)/admin/doors/+page.server.ts +++ b/src/routes/(app)/admin/doors/+layout.server.ts @@ -1,8 +1,8 @@ import apiNames from "$lib/utils/apiNames"; import { authorize } from "$lib/utils/authorization"; -import type { PageServerLoad } from "./$types"; +import type { LayoutServerLoad } from "./$types"; -export const load: PageServerLoad = async ({ locals }) => { +export const load: LayoutServerLoad = async ({ locals }) => { const { prisma, user } = locals; authorize(apiNames.DOOR.READ, user); From f8998c904dc47cd0b2e512dd0198ce81a8af22f5 Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:36:41 +0100 Subject: [PATCH 2/9] Implement /admin/doors layout --- .../(app)/admin/doors/+layout.server.ts | 3 +- src/routes/(app)/admin/doors/+layout.svelte | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/routes/(app)/admin/doors/+layout.svelte diff --git a/src/routes/(app)/admin/doors/+layout.server.ts b/src/routes/(app)/admin/doors/+layout.server.ts index 140fdaa65..732f5677f 100644 --- a/src/routes/(app)/admin/doors/+layout.server.ts +++ b/src/routes/(app)/admin/doors/+layout.server.ts @@ -2,12 +2,13 @@ import apiNames from "$lib/utils/apiNames"; import { authorize } from "$lib/utils/authorization"; import type { LayoutServerLoad } from "./$types"; -export const load: LayoutServerLoad = async ({ locals }) => { +export const load: LayoutServerLoad = async ({ locals, params }) => { const { prisma, user } = locals; authorize(apiNames.DOOR.READ, user); const doors = await prisma.door.findMany(); return { doors, + slug: params.slug, }; }; diff --git a/src/routes/(app)/admin/doors/+layout.svelte b/src/routes/(app)/admin/doors/+layout.svelte new file mode 100644 index 000000000..4c1c16c76 --- /dev/null +++ b/src/routes/(app)/admin/doors/+layout.svelte @@ -0,0 +1,46 @@ + + +
+

Doors

+ +
+ +
+
+ {#each doors as door (door.id)} + {@const isCurrent = door.name == selectedDoor} + +
+
+
+ {door.verboseName} +
+ + +
+
+
+ {/each} +
+
+ + +
+ {@render children()} +
+
+
From 6f2bc2620fae0d7eed04ab0f92f13e7f447c7090 Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:37:22 +0100 Subject: [PATCH 3/9] Remove whitespace in doors +page.server.ts --- .../admin/doors/edit/[slug]/+page.server.ts | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts index 0fabe0e68..f5c626516 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts @@ -13,32 +13,12 @@ export const load: PageServerLoad = async ({ locals, params }) => { const doorAccessPolicies = await prisma.doorAccessPolicy.findMany({ where: { doorName: params.slug, - OR: [ - { - endDatetime: { - gte: new Date(), - }, - }, - { - endDatetime: null, - }, - ], + OR: [{ endDatetime: { gte: new Date() } }, { endDatetime: null }], }, - include: { - member: true, - }, - orderBy: [ - { - startDatetime: "asc", - }, - { - role: "asc", - }, - { - studentId: "asc", - }, - ], + include: { member: true }, + orderBy: [{ startDatetime: "asc" }, { role: "asc" }, { studentId: "asc" }], }); + return { doorAccessPolicies, createForm: await superValidate(zod4(createSchema), { id: "createForm" }), From 2f6fdf1fc49c933d64e2cec20e08d447cb52dac3 Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:37:55 +0100 Subject: [PATCH 4/9] Add /admin/doors empty state page --- src/routes/(app)/admin/doors/+page.svelte | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/routes/(app)/admin/doors/+page.svelte b/src/routes/(app)/admin/doors/+page.svelte index 0a8eebb03..6f04679fa 100644 --- a/src/routes/(app)/admin/doors/+page.svelte +++ b/src/routes/(app)/admin/doors/+page.svelte @@ -1,5 +1,15 @@ - - + + +
+ +

+ Välj en dörr för att hantera åtkomstregler +

+
+
+
From 0542c9aab16d11e2a07dd018ef122be8b498167c Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Mon, 17 Nov 2025 23:26:24 +0100 Subject: [PATCH 5/9] Add door access rule form --- .../admin/doors/edit/[slug]/+page.server.ts | 82 +++++---- .../admin/doors/edit/[slug]/+page.svelte | 156 +++++++++++++++++- 2 files changed, 190 insertions(+), 48 deletions(-) diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts index f5c626516..e8dc79773 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts @@ -1,10 +1,11 @@ import apiNames from "$lib/utils/apiNames"; import { z } from "zod"; import type { Actions, PageServerLoad } from "./$types"; -import { message, setError, superValidate } from "sveltekit-superforms/server"; +import { message, superValidate } from "sveltekit-superforms/server"; import { zod4 } from "sveltekit-superforms/adapters"; import { fail } from "@sveltejs/kit"; import { authorize } from "$lib/utils/authorization"; +import authorizedPrismaClient from "$lib/server/authorizedPrisma"; export const load: PageServerLoad = async ({ locals, params }) => { const { prisma, user } = locals; @@ -21,23 +22,43 @@ export const load: PageServerLoad = async ({ locals, params }) => { return { doorAccessPolicies, - createForm: await superValidate(zod4(createSchema), { id: "createForm" }), - banForm: await superValidate(zod4(createSchema), { id: "banForm" }), + createForm: await superValidate(zod4(createSchema)), deleteForm: await superValidate(zod4(deleteSchema)), }; }; const createSchema = z .object({ - studentId: z.string().min(1).optional(), - role: z.string().min(1).optional(), + subject: z.string().min(1), + type: z.enum(["member", "role"]).default("member"), + mode: z.enum(["allow", "deny"]).default("allow"), startDatetime: z.date().optional(), endDatetime: z.date().optional(), - information: z.string().optional(), + reason: z.string().optional(), }) - .refine((data) => data.studentId != null || data.role != null, { - message: "Du måste ange roll eller student id", - }); + .refine( + (data) => (data.startDatetime ?? 0) < (data.endDatetime ?? Infinity), + { + message: "Slutdatum kan inte vara före startdatum", + path: ["endDatetime"], + }, + ) + .refine( + async ({ type, subject }) => { + if (type === "member") { + // check if member exists + return await authorizedPrismaClient.member.findFirst({ + where: { studentId: subject }, + }); + } else { + // check if role exists + return await authorizedPrismaClient.position.findFirst({ + where: { id: { startsWith: `${subject}%` } }, + }); + } + }, + { message: "Medlemmen/rollen finns inte", path: ["subject"] }, + ); const deleteSchema = z.object({ id: z.string(), @@ -49,47 +70,18 @@ export const actions: Actions = { const form = await superValidate(request, zod4(createSchema)); if (!form.valid) return fail(400, { form }); const doorName = params.slug; - const { studentId } = form.data; - if ( - studentId && - (await prisma.member.count({ - where: { studentId }, - })) <= 0 - ) { - return setError(form, "studentId", "Medlemmen finns inte"); - } - await prisma.doorAccessPolicy.create({ - data: { - doorName, - ...form.data, - }, - }); - return message(form, { - message: "Dörrpolicy skapad", - type: "success", - }); - }, - ban: async ({ request, locals, params }) => { - const { prisma } = locals; - const form = await superValidate(request, zod4(createSchema)); - if (!form.valid) return fail(400, { form }); - const doorName = params.slug; - const { studentId } = form.data; - if ( - studentId && - (await prisma.member.count({ - where: { studentId }, - })) <= 0 - ) { - return setError(form, "studentId", "Medlemmen finns inte"); - } + const { mode, subject, type, startDatetime, endDatetime } = form.data; + await prisma.doorAccessPolicy.create({ data: { doorName, - isBan: true, - ...form.data, + startDatetime, + endDatetime, + isBan: mode === "deny", + ...(type === "member" ? { studentId: subject } : { role: subject }), }, }); + return message(form, { message: "Dörrpolicy skapad", type: "success", diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte index 0a8eebb03..fc0459e20 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte @@ -1,5 +1,155 @@ - - +
+ + + + Add Access Rule + + Grant or restrict access to {page.params["slug"]} + + + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ + +
+
+
+ + +
From eaa616d03206053747d9322b2a98f91f9d703f4a Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Tue, 18 Nov 2025 00:14:41 +0100 Subject: [PATCH 6/9] Add door access list --- .../admin/doors/edit/[slug]/+page.server.ts | 2 + .../admin/doors/edit/[slug]/+page.svelte | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts index e8dc79773..5209ce226 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts @@ -27,6 +27,8 @@ export const load: PageServerLoad = async ({ locals, params }) => { }; }; +// TODO: require reason and end date for member rules +// TODO: don't allow banning groups since this is not implemented const createSchema = z .object({ subject: z.string().min(1), diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte index fc0459e20..85fc2baac 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte @@ -18,8 +18,12 @@ SelectTrigger, } from "$lib/components/ui/select"; import { superForm } from "$lib/utils/client/superForms"; + import TrashIcon from "@lucide/svelte/icons/trash"; + import { getFullName } from "$lib/utils/client/member"; + import { Badge } from "$lib/components/ui/badge"; let { data }: PageProps = $props(); + let policies = $derived(data.doorAccessPolicies); const { form, errors, constraints, enhance } = superForm(data.createForm); const types = [ @@ -52,6 +56,7 @@
+
@@ -152,4 +157,64 @@ + + + Current Rules + + {#if policies.length === 0} + No access policies configured yet + {:else} + {policies.length} rule{policies.length !== 1 ? "s" : ""} configured + {/if} + + + {#if policies.length > 0} + +
    + {#each policies as policy (policy.id)} + {@const type = policy.studentId === null ? "role" : "member"} +
  • +
    +
    +

    + {#if type === "member"} + {getFullName(policy.member!, { hideNickname: true })} + {:else} + {policy.role} + {/if} +

    + + {#if type === "member"} + Member + {:else} + Role + {/if} + + {#if policy.isBan} + Banned + {/if} + {#if policy.endDatetime} + + Expires {policy.endDatetime.toLocaleDateString("sv")} + + {/if} +
    + {#if policy.information} +

    + Reason: {policy.information} +

    + {/if} +
    + + + +
  • + + {/each} +
+
+ {/if} +
From e7525f0f0a72c756fa186712dd1241a7aec8d958 Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Tue, 18 Nov 2025 00:51:37 +0100 Subject: [PATCH 7/9] Add door access delete dialog --- .../admin/doors/edit/[slug]/+page.svelte | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte index 85fc2baac..30c9e3be6 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte @@ -1,6 +1,7 @@
@@ -205,12 +220,18 @@

{/if}
-
- - -
+ + {/each} @@ -218,3 +239,34 @@ {/if}
+ + + + {#if selectedPolicy} + + + Ta bort åtkomstregel + + Är du säher på att du vill ta bort åtkomstregeln för + {selectedPolicy.role || getFullName(selectedPolicy.member!)} + + till {selectedPolicy.doorName}? + + + + Cancel +
{ + open = false; + }} + > + + Continue +
+
+
+ {/if} +
From 43ace06257265c89bfe40f3b874f3e1b078a08d8 Mon Sep 17 00:00:00 2001 From: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com> Date: Wed, 26 Nov 2025 06:27:36 +0100 Subject: [PATCH 8/9] Refactor door access management - update translations - enhance error handling - improve UI components --- src/database/seed/data.ts | 1 - src/routes/(app)/admin/doors/+error.svelte | 16 ++++ src/routes/(app)/admin/doors/+layout.svelte | 13 ++-- src/routes/(app)/admin/doors/+page.svelte | 3 +- .../admin/doors/edit/[slug]/+page.server.ts | 19 +++-- .../admin/doors/edit/[slug]/+page.svelte | 77 +++++++++++-------- src/translations/en.json | 37 ++++++--- src/translations/sv.json | 37 ++++++--- 8 files changed, 132 insertions(+), 71 deletions(-) create mode 100644 src/routes/(app)/admin/doors/+error.svelte diff --git a/src/database/seed/data.ts b/src/database/seed/data.ts index 241b45afb..15c4606d7 100644 --- a/src/database/seed/data.ts +++ b/src/database/seed/data.ts @@ -218,7 +218,6 @@ export const models: SeedClientOptions["models"] = { }, doorAccessPolicy: { data: { - role: "dsek", startDatetime: () => faker.helpers.maybe(() => faker.date.past()) ?? null, endDatetime: (ctx) => ctx.data.startDatetime && Math.random() > 0.5 diff --git a/src/routes/(app)/admin/doors/+error.svelte b/src/routes/(app)/admin/doors/+error.svelte new file mode 100644 index 000000000..c2e95ea9e --- /dev/null +++ b/src/routes/(app)/admin/doors/+error.svelte @@ -0,0 +1,16 @@ + + + + +
+ +

+ {page.status}: {page.error?.message} +

+
+
+
diff --git a/src/routes/(app)/admin/doors/+layout.svelte b/src/routes/(app)/admin/doors/+layout.svelte index 4c1c16c76..4ada03aa6 100644 --- a/src/routes/(app)/admin/doors/+layout.svelte +++ b/src/routes/(app)/admin/doors/+layout.svelte @@ -2,6 +2,7 @@ import type { LayoutProps } from "./$types"; import { CardTitle } from "$lib/components/ui/card"; import DoorOpen from "@lucide/svelte/icons/door-open"; + import * as m from "$paraglide/messages"; let { data, children }: LayoutProps = $props(); let doors = $derived(data.doors); @@ -9,18 +10,18 @@
-

Doors

+

{m.doors()}

-
+
    {#each doors as door (door.id)} {@const isCurrent = door.name == selectedDoor} -
    -
+ {/each} -
+ diff --git a/src/routes/(app)/admin/doors/+page.svelte b/src/routes/(app)/admin/doors/+page.svelte index 6f04679fa..5420067cd 100644 --- a/src/routes/(app)/admin/doors/+page.svelte +++ b/src/routes/(app)/admin/doors/+page.svelte @@ -1,6 +1,7 @@ @@ -8,7 +9,7 @@

- Välj en dörr för att hantera åtkomstregler + {m.admin_doors_choose()}

diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts index 5209ce226..14bc2a184 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts @@ -3,14 +3,20 @@ import { z } from "zod"; import type { Actions, PageServerLoad } from "./$types"; import { message, superValidate } from "sveltekit-superforms/server"; import { zod4 } from "sveltekit-superforms/adapters"; -import { fail } from "@sveltejs/kit"; +import { error, fail } from "@sveltejs/kit"; import { authorize } from "$lib/utils/authorization"; import authorizedPrismaClient from "$lib/server/authorizedPrisma"; +import * as m from "$paraglide/messages"; -export const load: PageServerLoad = async ({ locals, params }) => { +export const load: PageServerLoad = async ({ locals, params, parent }) => { const { prisma, user } = locals; authorize(apiNames.DOOR.READ, user); + const { doors } = await parent(); + const door = doors.find((door) => door.name === params.slug); + + if (!door) error(404, m.admin_doors_notFound()); + const doorAccessPolicies = await prisma.doorAccessPolicy.findMany({ where: { doorName: params.slug, @@ -21,6 +27,7 @@ export const load: PageServerLoad = async ({ locals, params }) => { }); return { + door, doorAccessPolicies, createForm: await superValidate(zod4(createSchema)), deleteForm: await superValidate(zod4(deleteSchema)), @@ -41,7 +48,7 @@ const createSchema = z .refine( (data) => (data.startDatetime ?? 0) < (data.endDatetime ?? Infinity), { - message: "Slutdatum kan inte vara före startdatum", + message: m.admin_doors_endDateBeforeStart(), path: ["endDatetime"], }, ) @@ -59,7 +66,7 @@ const createSchema = z }); } }, - { message: "Medlemmen/rollen finns inte", path: ["subject"] }, + { message: m.admin_doors_memberOrRoleNotFound(), path: ["subject"] }, ); const deleteSchema = z.object({ @@ -85,7 +92,7 @@ export const actions: Actions = { }); return message(form, { - message: "Dörrpolicy skapad", + message: m.admin_doors_ruleCreated(), type: "success", }); }, @@ -98,7 +105,7 @@ export const actions: Actions = { where: { id }, }); return message(form, { - message: "Dörrpolicy raderad", + message: m.admin_doors_ruleDeleted(), type: "success", }); }, diff --git a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte index 30c9e3be6..448221b4a 100644 --- a/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte +++ b/src/routes/(app)/admin/doors/edit/[slug]/+page.svelte @@ -1,6 +1,5 @@