Skip to content

Commit

Permalink
feat(Roles, Career): implement role inheritance
Browse files Browse the repository at this point in the history
  • Loading branch information
simonknittel committed Jan 13, 2025
1 parent ded9852 commit 3e4cb50
Show file tree
Hide file tree
Showing 21 changed files with 474 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "_inheritance" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "_inheritance_AB_unique" ON "_inheritance"("A", "B");

-- CreateIndex
CREATE INDEX "_inheritance_B_index" ON "_inheritance"("B");

-- AddForeignKey
ALTER TABLE "_inheritance" ADD CONSTRAINT "_inheritance_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_inheritance" ADD CONSTRAINT "_inheritance_B_fkey" FOREIGN KEY ("B") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
4 changes: 3 additions & 1 deletion app/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ model Role {
thumbnail Upload? @relation(name: "thumbnail", fields: [thumbnailId], references: [id], onDelete: SetNull)
permissionStrings PermissionString[]
flowNodes FlowNode[] @relation("flowNodes")
inherits Role[] @relation("inheritance")
inheritedBy Role[] @relation("inheritance")
}

model PermissionString {
Expand Down Expand Up @@ -410,7 +412,7 @@ model Flow {
id String @id
name String
nodes FlowNode[]
position Int? @unique @default(autoincrement())
position Int @unique @default(autoincrement())
}

model FlowNode {
Expand Down
7 changes: 4 additions & 3 deletions app/src/app/app/events/[id]/fleet/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export default async function Page({ params }: Props) {
await authentication.authorizePage("event", "read");
await authentication.authorizePage("orgFleet", "read");

const event = await getEvent((await params).id);
const eventId = (await params).id;
const event = await getEvent(eventId);

return (
<main className="p-4 pb-20 lg:p-8 max-w-[1920px] mx-auto">
Expand All @@ -52,9 +53,9 @@ export default async function Page({ params }: Props) {
</div>

<Navigation
eventId={event.data.id}
eventId={eventId}
participantsCount={event.data.user_count}
active="/fleet"
active={`/app/events/${eventId}/fleet`}
className="mt-4"
/>

Expand Down
7 changes: 4 additions & 3 deletions app/src/app/app/events/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export default async function Page({ params }: Props) {
const authentication = await authenticatePage("/app/events/[id]");
await authentication.authorizePage("event", "read");

const event = await getEvent((await params).id);
const eventId = (await params).id;
const event = await getEvent(eventId);

return (
<main className="p-4 pb-20 lg:p-8 max-w-[1920px] mx-auto">
Expand All @@ -51,9 +52,9 @@ export default async function Page({ params }: Props) {
</div>

<Navigation
eventId={event.data.id}
eventId={eventId}
participantsCount={event.data.user_count}
active=""
active={`/app/events/${eventId}`}
className="mt-4"
/>

Expand Down
7 changes: 4 additions & 3 deletions app/src/app/app/events/[id]/participants/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export default async function Page({ params }: Props) {
const authentication = await authenticatePage("/app/events/[id]");
await authentication.authorizePage("event", "read");

const event = await getEvent((await params).id);
const eventId = (await params).id;
const event = await getEvent(eventId);

return (
<main className="p-4 pb-20 lg:p-8 max-w-[1920px] mx-auto">
Expand All @@ -51,9 +52,9 @@ export default async function Page({ params }: Props) {
</div>

<Navigation
eventId={event.data.id}
eventId={eventId}
participantsCount={event.data.user_count}
active="/participants"
active={`/app/events/${eventId}/participants`}
className="mt-4"
/>

Expand Down
82 changes: 82 additions & 0 deletions app/src/app/app/roles/[id]/inheritance/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { authenticatePage } from "@/auth/server";
import { SingleRole } from "@/common/components/SingleRole";
import { log } from "@/logging";
import { InheritanceForm } from "@/roles/components/InheritanceForm";
import { Navigation } from "@/roles/components/Navigation";
import { getRoleById, getRoles } from "@/roles/queries";
import { type Metadata } from "next";
import { notFound } from "next/navigation";
import { serializeError } from "serialize-error";

type Params = Promise<{
id: string;
}>;

export async function generateMetadata(props: {
params: Params;
}): Promise<Metadata> {
try {
const role = await getRoleById((await props.params).id);

return {
title: `${role?.name} - Vererbungen | S.A.M. - Sinister Incorporated`,
};
} catch (error) {
void log.error(
"Error while generating metadata for /app/roles/[id]/inheritance/page.tsx",
{
error: serializeError(error),
},
);

return {
title: `Error | S.A.M. - Sinister Incorporated`,
};
}
}

type Props = Readonly<{
params: Params;
}>;

export default async function Page({ params }: Props) {
const authentication = await authenticatePage("/app/roles");
await authentication.authorizePage("role", "manage");

const roleId = (await params).id;
const role = await getRoleById(roleId);
if (!role) notFound();

const roles = await getRoles();
const _roles = roles
.filter((r) => r.id !== role.id)
.toSorted((a, b) => a.name.localeCompare(b.name));

return (
<main className="p-4 pb-20 lg:p-8 max-w-[1920px] mx-auto">
<div className="flex gap-2 font-bold text-xl">
<span className="text-neutral-500">Rolle /</span>
<p>{role?.name}</p>
</div>

<Navigation
role={role}
active={`/app/roles/${roleId}/inheritance`}
className="mt-2"
/>

<section className="rounded-2xl bg-neutral-800/50 p-4 lg:p-8 mt-4">
<h2 className="text-xl font-bold mb-2">Vererbungen</h2>
<p>
Die Rolle <SingleRole role={role} className="inline-flex align-sub" />{" "}
erhält alle Berechtigungen von den folgenden ausgewählten Rollen. Im
Karrieresystem gelten die folgenden Rollen ebenfalls als
freigeschaltet. Verschachtelte Vererbungen werden nicht
berücksichtigt.
</p>

<InheritanceForm currentRole={role} roles={_roles} className="mt-4" />
</section>
</main>
);
}
36 changes: 8 additions & 28 deletions app/src/app/app/roles/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { authenticatePage } from "@/auth/server";
import { Link } from "@/common/components/Link";
import { log } from "@/logging";
import { Navigation } from "@/roles/components/Navigation";
import { OverviewTab } from "@/roles/components/OverviewTab";
import { getRoleById } from "@/roles/queries";
import { type Metadata } from "next";
import { notFound } from "next/navigation";
import { FaHome, FaLock, FaUsers } from "react-icons/fa";
import { serializeError } from "serialize-error";

type Params = Promise<{
Expand Down Expand Up @@ -43,7 +42,8 @@ export default async function Page({ params }: Props) {
const authentication = await authenticatePage("/app/roles");
await authentication.authorizePage("role", "manage");

const role = await getRoleById((await params).id);
const roleId = (await params).id;
const role = await getRoleById(roleId);
if (!role) notFound();

return (
Expand All @@ -53,31 +53,11 @@ export default async function Page({ params }: Props) {
<p>{role?.name}</p>
</div>

<div className="flex flex-wrap mt-2">
<Link
href={`/app/roles/${role.id}`}
className="first:rounded-l border-[1px] border-sinister-red-500 last:rounded-r h-8 flex items-center justify-center px-3 gap-2 uppercase bg-sinister-red-500 text-white"
>
<FaHome />
Übersicht
</Link>

<Link
href={`/app/roles/${role.id}/permissions`}
className="first:rounded-l border-[1px] border-sinister-red-500 last:rounded-r h-8 flex items-center justify-center px-3 gap-2 uppercase text-sinister-red-500 hover:text-sinister-red-300 hover:border-sinister-red-300"
>
<FaLock />
Berechtigungen
</Link>

<Link
href={`/app/spynet/citizen?filters=role-${role.id}`}
className="first:rounded-l border-[1px] border-sinister-red-500 last:rounded-r h-8 flex items-center justify-center px-3 gap-2 uppercase text-sinister-red-500 hover:text-sinister-red-300 hover:border-sinister-red-300"
>
<FaUsers />
Citizen
</Link>
</div>
<Navigation
role={role}
active={`/app/roles/${roleId}`}
className="mt-2"
/>

<OverviewTab role={role} className="mt-2" />
</main>
Expand Down
36 changes: 8 additions & 28 deletions app/src/app/app/roles/[id]/permissions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { authenticatePage } from "@/auth/server";
import { Link } from "@/common/components/Link";
import { dedupedGetUnleashFlag } from "@/common/utils/getUnleashFlag";
import { log } from "@/logging";
import { Navigation } from "@/roles/components/Navigation";
import { PermissionsTab } from "@/roles/components/PermissionsTab";
import { getRoleById, getRoles } from "@/roles/queries";
import { getAllClassificationLevels, getAllNoteTypes } from "@/spynet/queries";
import { type Metadata } from "next";
import { notFound } from "next/navigation";
import { FaHome, FaLock, FaUsers } from "react-icons/fa";
import { serializeError } from "serialize-error";

type Params = Promise<{
Expand Down Expand Up @@ -45,7 +44,8 @@ export default async function Page({ params }: Props) {
const authentication = await authenticatePage("/app/roles");
await authentication.authorizePage("role", "manage");

const role = await getRoleById((await params).id);
const roleId = (await params).id;
const role = await getRoleById(roleId);
if (!role) notFound();

const [allRoles, noteTypes, classificationLevels] = await Promise.all([
Expand All @@ -65,31 +65,11 @@ export default async function Page({ params }: Props) {
<p>{role?.name}</p>
</div>

<div className="flex flex-wrap mt-2">
<Link
href={`/app/roles/${role.id}`}
className="first:rounded-l border-[1px] border-sinister-red-500 last:rounded-r h-8 flex items-center justify-center px-3 gap-2 uppercase text-sinister-red-500 hover:text-sinister-red-300 hover:border-sinister-red-300"
>
<FaHome />
Übersicht
</Link>

<Link
href={`/app/roles/${role.id}/permissions`}
className="first:rounded-l border-[1px] border-sinister-red-500 last:rounded-r h-8 flex items-center justify-center px-3 gap-2 uppercase bg-sinister-red-500 text-white"
>
<FaLock />
Berechtigungen
</Link>

<Link
href={`/app/spynet/citizen?filters=role-${role.id}`}
className="first:rounded-l border-[1px] border-sinister-red-500 last:rounded-r h-8 flex items-center justify-center px-3 gap-2 uppercase text-sinister-red-500 hover:text-sinister-red-300 hover:border-sinister-red-300"
>
<FaUsers />
Citizen
</Link>
</div>
<Navigation
role={role}
active={`/app/roles/${roleId}/permissions`}
className="mt-2"
/>

<PermissionsTab
role={role}
Expand Down
2 changes: 1 addition & 1 deletion app/src/auth/getPermissionSetsByRoles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const getPermissionSetsByRoles = (roles: Roles): PermissionSet[] => {
permissionSet.attributes = attributeStrings.map((attributeString) => {
const [key, value] = attributeString.split("=");

if (!key || !value) throw new Error("Invalid attributeString string");
if (!key || !value) throw new Error("Invalid attributeString");

return { key, value };
});
Expand Down
19 changes: 16 additions & 3 deletions app/src/auth/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,22 @@ export const authOptions: NextAuthOptions = {

const roles = await prisma.role.findMany({
where: {
id: {
in: assignedRoles,
},
OR: [
{
id: {
in: assignedRoles,
},
},
{
inheritedBy: {
some: {
id: {
in: assignedRoles,
},
},
},
},
],
},
include: {
permissionStrings: true,
Expand Down
4 changes: 3 additions & 1 deletion app/src/career/components/Flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ type Props = Readonly<{
})[];
};
roles: Role[];
assignedRoles: Role[];
assignedRoles: (Role & {
inherits: Role[];
})[];
canUpdate?: boolean;
isUpdating?: boolean;
}>;
Expand Down
Loading

0 comments on commit 3e4cb50

Please sign in to comment.