Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/database/seed/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/routes/(app)/admin/doors/+error.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import { page } from "$app/state";
import { Card, CardContent } from "$lib/components/ui/card";
import Error from "@lucide/svelte/icons/octagon-x";
</script>

<Card>
<CardContent>
<div class="flex flex-col items-center gap-8 py-8">
<Error size={96} class="text-muted-foreground" />
<h1 class="text-muted-foreground">
{page.status}: {page.error?.message}
</h1>
</div>
</CardContent>
</Card>
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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, params }) => {
const { prisma, user } = locals;
authorize(apiNames.DOOR.READ, user);

const doors = await prisma.door.findMany();
return {
doors,
slug: params.slug,
};
};
47 changes: 47 additions & 0 deletions src/routes/(app)/admin/doors/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
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);
let selectedDoor = $derived(data.slug);
</script>

<main class="layout-container">
<h3 class="mb-3">{m.doors()}</h3>

<div class="grid grid-cols-1 gap-8 lg:grid-cols-[auto_1fr]">
<!-- Left column: list of doors -->
<section>
<ul class="m-0 space-y-2">
{#each doors as door (door.id)}
{@const isCurrent = door.name == selectedDoor}
<a href="/admin/doors/edit/{door.name}" class="block">
<li
class={{
"list-none rounded-lg border p-4 px-6": true,
"ring-rosa-background ring-2": isCurrent,
"hover:border-rosa-hover": !isCurrent,
}}
>
<div class="flex items-center justify-between gap-16">
<div>
<CardTitle>{door.verboseName}</CardTitle>
</div>

<DoorOpen />
</div>
</li>
</a>
{/each}
</ul>
</section>

<!-- Right column: policies for chosen door -->
<section>
{@render children()}
</section>
</div>
</main>
17 changes: 14 additions & 3 deletions src/routes/(app)/admin/doors/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
<script>
import NotImplemented from "$lib/components/NotImplemented.svelte";
<script lang="ts">
import { Card, CardContent } from "$lib/components/ui/card";
import DoorClosed from "@lucide/svelte/icons/door-closed";
import * as m from "$paraglide/messages";
</script>

<NotImplemented />
<Card>
<CardContent>
<div class="flex flex-col items-center gap-8 py-8">
<DoorClosed size={96} class="text-muted-foreground" />
<p class="text-muted-foreground">
{m.admin_doors_choose()}
</p>
</div>
</CardContent>
</Card>
166 changes: 91 additions & 75 deletions src/routes/(app)/admin/doors/edit/[slug]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,100 @@
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 { error, fail } from "@sveltejs/kit";
import { authorize } from "$lib/utils/authorization";
import authorizedPrismaClient from "$lib/server/authorizedPrisma";
import * as m from "$paraglide/messages";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);

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,
OR: [
{
endDatetime: {
gte: new Date(),
},
},
{
endDatetime: null,
},
],
},
include: {
member: true,
OR: [{ endDatetime: { gte: new Date() } }, { endDatetime: null }],
},
orderBy: [
{
startDatetime: "asc",
},
{
role: "asc",
},
{
studentId: "asc",
},
],
include: { member: true },
orderBy: [{ startDatetime: "asc" }, { role: "asc" }, { studentId: "asc" }],
});

return {
door,
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(),
startDatetime: z.date().optional(),
endDatetime: z.date().optional(),
information: z.string().optional(),
subject: z.string().min(1),
type: z.enum(["member", "role"]).default("member"),
mode: z.enum(["allow", "deny"]).default("allow"),
startDatetime: z.string().date().optional(),
endDatetime: z.string().date().optional(),
reason: z.string().optional(),
})
.refine((data) => data.studentId != null || data.role != null, {
message: "Du måste ange roll eller student id",
});
// These refinements return true for valid data, but it's
// easier to express them in terms of what is invalid.
.refine(
// Require the start date to be before the end date
({ startDatetime: start, endDatetime: end }) =>
!(start && end && dayjs(end).isBefore(start)),
{ message: m.admin_doors_endDateBeforeStart(), path: ["endDatetime"] },
)
.refine(
// Require an end date for member rules
(data) => !(data.type === "member" && !data.endDatetime),
{ message: m.admin_doors_memberRuleRequireEnd(), path: ["endDatetime"] },
)
.refine(
// Require a reason for member rules
(data) => !(data.type === "member" && !data.reason),
{ message: m.admin_doors_memberRuleRequireReason(), path: ["reason"] },
)
.refine(
// Require a reason for bans
(data) => !(data.mode === "deny" && !data.reason),
{ message: m.admin_doors_banRuleRequireReason(), path: ["reason"] },
)
.refine(
// TODO: Banning groups is not implemented
(data) => !(data.type === "role" && data.mode === "deny"),
{ message: "Not implemented", path: ["mode"] },
)
.refine(
async (data) => {
if (data.type === "member") {
// check if member exists
return await authorizedPrismaClient.member.findFirst({
where: { studentId: data.subject },
});
} else {
// check if role exists
return (
data.subject === "*" ||
(await authorizedPrismaClient.position.findFirst({
where: { id: { startsWith: `${data.subject}%` } },
}))
);
}
},
{ message: m.admin_doors_memberOrRoleNotFound(), path: ["subject"] },
);

const deleteSchema = z.object({
id: z.string(),
Expand All @@ -69,49 +106,28 @@ 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, reason } =
form.data;

await prisma.doorAccessPolicy.create({
data: {
doorName,
isBan: true,
...form.data,
startDatetime: dayjs(startDatetime)
.startOf("day")
.tz("Europe/Stockholm", true)
.toDate(),
endDatetime: dayjs(endDatetime)
.endOf("day")
.tz("Europe/Stockholm", true)
.toDate(),
isBan: mode === "deny",
information: reason,
...(type === "member" ? { studentId: subject } : { role: subject }),
},
});

return message(form, {
message: "Dörrpolicy skapad",
message: m.admin_doors_ruleCreated(),
type: "success",
});
},
Expand All @@ -124,7 +140,7 @@ export const actions: Actions = {
where: { id },
});
return message(form, {
message: "Dörrpolicy raderad",
message: m.admin_doors_ruleDeleted(),
type: "success",
});
},
Expand Down
Loading
Loading