Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve door access query #261

Merged
merged 4 commits into from
May 2, 2024
Merged
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
216 changes: 124 additions & 92 deletions src/lib/utils/member.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PrismaClient } from "@prisma/client";
import { getDerivedRoles } from "./authorization";
import { error } from "@sveltejs/kit";
import { getDerivedRoles } from "./authorization";

export const getCustomAuthorOptions = async (
prisma: PrismaClient,
Expand Down Expand Up @@ -39,117 +39,149 @@ export const getCustomAuthorOptions = async (
});
};

export type MemberDoorPolicies = Array<{
name: string;
verboseName: string | undefined;
roles: string[];
startDate: Date | null;
endDate: Date | null;
}>;

export const getCurrentDoorPoliciesForMember = async (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this function could use some tests, but it's not blocking IMO

prisma: PrismaClient,
memberId: string,
studentId: string,
) => {
const [memberResult] = await Promise.allSettled([
prisma.member.findUnique({
where: {
id: memberId,
const memberPositionIds = await prisma.position
.findMany({
select: {
id: true,
name: true,
boardMember: true,
},
include: {
where: {
mandates: {
where: {
some: {
member: {
studentId,
},
startDate: {
lte: new Date(),
},
endDate: {
gte: new Date(),
},
},
include: {
position: {},
},
},
doorAccessPolicies: {},
},
}),
]);

if (memberResult.status === "rejected") {
throw error(500, "Could not fetch member");
}
if (!memberResult.value) {
throw error(404, "Member not found");
}

const member = memberResult.value;
const allDoorPolicies = await prisma.doorAccessPolicy.findMany();

const roles = member.doorAccessPolicies.map((d) => d.role).filter(notEmpty);
const positions = (
await prisma.position.findMany({
})
.catch(() => {
throw error(500, "Could not fetch member positions");
});
const userDoorPolicies = await prisma.doorAccessPolicy
.findMany({
where: {
id: {
in: roles,
},
AND: [
{
// is active, or indefinite
OR: [
{
startDatetime: null,
},
{
startDatetime: {
lte: new Date(),
},
},
],
},
{
// is active, or indefinite
OR: [
{
endDatetime: null,
},
{
endDatetime: {
gte: new Date(),
},
},
],
},
{
OR: [
{
studentId, // is for this user
},
{
role: {
in: getDerivedRoles(
memberPositionIds.map((pos) => pos.id),
true,
).concat(
memberPositionIds.some((pos) => pos.boardMember)
? ["dsek.styr"]
: [],
),
},
},
],
},
],
},
})
).concat(member.mandates.map((m) => m.position));
.catch(() => {
throw error(500, "Could not fetch door access");
});

// Map a doorname to roles, startDate and endDate
const allMemberDoors = new Map<
string,
{
roles: string[];
startDate: Date | null;
endDate: Date | null;
}
>();
const doors = await prisma.door.findMany();

allDoorPolicies
.filter((doorPolicy) =>
member.mandates.some(
(mandate) =>
// A doorpolicy is associated with either a role or a specific member
(doorPolicy.role && mandate.positionId.startsWith(doorPolicy.role)) ||
doorPolicy.studentId === member.studentId,
),
)
.forEach((doorPolicy) => {
// Get a nice name for a position instead of using the id
const positionNamesFromMandates = positions
.filter(
(position) =>
doorPolicy.role && position.id.startsWith(doorPolicy.role),
const policiesByDoor: MemberDoorPolicies = userDoorPolicies.reduce(
(acc, policy) => {
const role = policy.role ?? "Du";
const duplicate = acc.find(
(p) =>
p.name === policy.doorName &&
p.startDate === policy.startDatetime &&
p.endDate === policy.endDatetime,
);
if (duplicate) {
duplicate.roles.push(role);
return acc;
}
acc.push({
name: policy.doorName,
verboseName: doors.find((door) => door.name == policy.doorName)
?.verboseName,
roles: [role],
startDate: policy.startDatetime,
endDate: policy.endDatetime,
});
return acc;
},
[] as MemberDoorPolicies,
);
const memberDoorPolicies: MemberDoorPolicies = policiesByDoor.map(
(policy) => {
const positionsMappedToThisDoor = memberPositionIds
.filter((pos) =>
policy.roles.some(
(role) =>
pos.id.startsWith(role) ||
(pos.boardMember && role === "dsek.styr"),
),
)
.map((position) => position.name);
const positionNames: string[] =
positionNamesFromMandates.length > 0
? positionNamesFromMandates
: ["Du"];

const oldData = allMemberDoors.get(doorPolicy.doorName);
const newData = oldData ?? {
roles: [...positionNames],
startDate: doorPolicy.startDatetime,
endDate: doorPolicy.endDatetime,
.map((pos) => pos.name);
positionsMappedToThisDoor.sort();
return {
...policy,
roles:
positionsMappedToThisDoor.length > 0
? positionsMappedToThisDoor
: ["Du"],
};
if (oldData) {
// Remove duplicates
newData.roles = [...new Set(oldData.roles.concat(...positionNames))];

if (doorPolicy.startDatetime) {
newData.startDate =
oldData.startDate === null ||
doorPolicy.startDatetime > oldData.startDate
? oldData.startDate
: doorPolicy.startDatetime;
}
if (doorPolicy.endDatetime) {
newData.endDate =
oldData.endDate === null || doorPolicy.endDatetime < oldData.endDate
? oldData.endDate
: doorPolicy.endDatetime;
}
}
allMemberDoors.set(doorPolicy.doorName, newData);
});
},
);
memberDoorPolicies.sort((a, b) => a.name.localeCompare(b.name));

return allMemberDoors;
return memberDoorPolicies;
};

function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
18 changes: 10 additions & 8 deletions src/routes/(app)/members/[studentId]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { getCurrentDoorPoliciesForMember } from "$lib/utils/member";
import keycloak from "$lib/server/keycloak";

export const load: PageServerLoad = async ({ locals, params }) => {
const { user, prisma } = locals;
const { prisma, user } = locals;
const { studentId } = params;
const [memberResult, publishedArticlesResult] = await Promise.allSettled([
prisma.member.findUnique({
where: {
studentId: params.studentId,
studentId: studentId,
},
include: {
mandates: {
Expand Down Expand Up @@ -43,7 +44,7 @@ export const load: PageServerLoad = async ({ locals, params }) => {
where: {
author: {
member: {
studentId: params.studentId,
studentId: studentId,
},
},
removedAt: null,
Expand All @@ -64,10 +65,11 @@ export const load: PageServerLoad = async ({ locals, params }) => {
throw error(404, "Member not found");
}
const member = memberResult.value;
const allMemberDoors = await getCurrentDoorPoliciesForMember(
prisma,
member.id,
);

const doorAccess =
member.id === user?.memberId
? await getCurrentDoorPoliciesForMember(prisma, studentId)
: [];

const email =
member.studentId !== null
Expand All @@ -79,7 +81,7 @@ export const load: PageServerLoad = async ({ locals, params }) => {
form: await superValidate(member, memberSchema),
pingForm: await superValidate(emptySchema),
viewedMember: member, // https://github.com/Dsek-LTH/web/issues/194
allMemberDoors,
doorAccess,
publishedArticles: publishedArticlesResult.value ?? [],
email,
ping: user
Expand Down
29 changes: 4 additions & 25 deletions src/routes/(app)/members/[studentId]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import DoorAccess from "./DoorAccess.svelte";
import HeldPositionsYear from "./HeldPositionsYear.svelte";
import PublishedArticles from "./PublishedArticles.svelte";
import PublishedEvents from "./PublishedEvents.svelte";
Expand Down Expand Up @@ -135,32 +136,10 @@
</div>
</div>
<div
class="col-span-5 sm:col-span-3 sm:col-start-3 md:col-start-1 lg:col-span-2 lg:col-start-1 xl:col-span-1 xl:col-start-1"
class="col-span-5 sm:col-span-3 sm:col-start-3 md:col-start-1 lg:col-span-2 lg:col-start-1 xl:col-span-2 xl:col-start-1"
>
{#if isMe && data.allMemberDoors.size > 0}
<br />
<div class="my-2 text-xl font-bold">Dörrar</div>
{#each Array.from(data.allMemberDoors.entries()) as [doorName, doorData]}
<div class="my-2 flex justify-between rounded-lg bg-base-200 p-3">
<div class="my-auto font-bold">
{doorName}
</div>
<div class="flex flex-col text-right">
<span class="text-xs font-bold opacity-50">
{doorData.roles.join(" ")}
</span>
{#if doorData.startDate != null || doorData.endDate != null}
<div>
<span class="text-xs font-bold opacity-50">
{doorData.startDate?.toLocaleDateString() ?? ""}
-
{doorData.endDate?.toLocaleDateString() ?? ""}
</span>
</div>
{/if}
</div>
</div>
{/each}
{#if data.doorAccess.length > 0}
<DoorAccess doorAccess={data.doorAccess} />
{/if}
</div>
</article>
31 changes: 31 additions & 0 deletions src/routes/(app)/members/[studentId]/DoorAccess.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import type { MemberDoorPolicies } from "$lib/utils/member";
export let doorAccess: MemberDoorPolicies;
</script>

<div class="my-2 text-xl font-bold">Dörraccess</div>
{#each doorAccess as doorPolicy (doorPolicy.name + doorPolicy.startDate + doorPolicy.endDate)}
<div
class="my-2 flex items-center justify-between gap-4 rounded-lg bg-base-200 p-3"
>
<div class="flex flex-col">
<span class="font-semibold">{doorPolicy.verboseName}</span>
{#if doorPolicy.startDate != null || doorPolicy.endDate != null}
<span class="text-nowrap text-[0.5rem] font-semibold opacity-50">
{doorPolicy.startDate?.toLocaleDateString("sv-SE") ?? ""}
-
{doorPolicy.endDate?.toLocaleDateString("sv-SE") ?? ""}
</span>
{/if}
</div>
<div class="flex flex-1 flex-col items-stretch overflow-hidden text-right">
<div class="flex flex-col text-xs font-bold opacity-50">
{#each doorPolicy.roles as role (role)}
<span class="overflow-hidden text-ellipsis whitespace-nowrap"
>{role}</span
>
{/each}
</div>
</div>
</div>
{/each}
Loading