Support missing roles and deleted user

This commit is contained in:
2025-04-01 23:26:56 +02:00
parent fc302d8ef2
commit 3694492e1a
10 changed files with 88 additions and 47 deletions

View File

@@ -274,7 +274,7 @@ const app = new Elysia()
);
return Option.match(res, {
onNone: () => error("Not Found", undefined),
onNone: () => error("Not Found", new Response() as unknown as void),
onSome: ({ displayName }) => ({ userId, displayName }),
});
}, {
@@ -290,13 +290,13 @@ const app = new Elysia()
// --- MARK: PIECE CRUD ------------------------------------------------
.post("/piece", async ({ db, body: { name, composer, lyricist, arranger }, session: { idToken } }) => {
.post("/piece", async ({ db, body: { name, composer, lyricist, arranger }, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -369,7 +369,7 @@ const app = new Elysia()
.executeTakeFirst();
if (piece === undefined) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
const attachments = await db
@@ -396,13 +396,13 @@ const app = new Elysia()
},
})
.put("/piece/:pieceId", async ({ db, body: { name, composer, lyricist, arranger }, params: { pieceId }, session: { idToken } }) => {
.put("/piece/:pieceId", async ({ db, body: { name, composer, lyricist, arranger }, params: { pieceId }, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -414,7 +414,7 @@ const app = new Elysia()
.execute();
if (res.length === 0) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
const attachments = await db
@@ -448,13 +448,13 @@ const app = new Elysia()
},
})
.delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, session: { idToken } }) => {
.delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -465,7 +465,7 @@ const app = new Elysia()
.execute();
if (res.length === 0) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
set.status = "No Content";
@@ -482,13 +482,13 @@ const app = new Elysia()
// --- MARK: ATTACHMENT CRUD -------------------------------------------
.post("/piece/:pieceId/attachment", async ({ db, body: { filename, mediaType, data }, params: { pieceId }, session: { idToken } }) => {
.post("/piece/:pieceId/attachment", async ({ db, body: { filename, mediaType, data }, params: { pieceId }, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -554,7 +554,7 @@ const app = new Elysia()
.executeTakeFirst();
if (res === undefined) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
set.headers["content-disposition"] = `attachment; filename*=UTF-8''${encodeURIComponent(res.filename)}`;
@@ -572,13 +572,13 @@ const app = new Elysia()
},
})
.put("/piece/:pieceId/attachment/:attachmentId", async ({ db, body: { filename }, params: { pieceId, attachmentId }, session: { idToken } }) => {
.put("/piece/:pieceId/attachment/:attachmentId", async ({ db, body: { filename }, params: { pieceId, attachmentId }, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -593,7 +593,7 @@ const app = new Elysia()
.execute();
if (res.length === 0) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
return {
@@ -616,13 +616,13 @@ const app = new Elysia()
},
})
.delete("/piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, set, session: { idToken } }) => {
.delete("/piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, set, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -636,7 +636,7 @@ const app = new Elysia()
.execute();
if (res.length === 0) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
set.status = "No Content";
@@ -655,13 +655,13 @@ const app = new Elysia()
// --- MARK: REPERTOIRE CRUD -------------------------------------------
.post("/repertoire", async ({ db, body: { name, entries }, session: { idToken } }) => {
.post("/repertoire", async ({ db, body: { name, entries }, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -741,7 +741,7 @@ const app = new Elysia()
.executeTakeFirst();
if (repertoire === undefined) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
const entries = await db
@@ -766,13 +766,13 @@ const app = new Elysia()
},
})
.put("/repertoire/:repertoireId", async ({ db, body: { name, entries }, params: { repertoireId }, session: { idToken } }) => {
.put("/repertoire/:repertoireId", async ({ db, body: { name, entries }, params: { repertoireId }, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}
@@ -784,7 +784,7 @@ const app = new Elysia()
.execute();
if (res.length === 0) {
return error("Not Found", undefined);
return error("Not Found", new Response() as unknown as void);
}
await db
@@ -821,13 +821,13 @@ const app = new Elysia()
},
})
.delete("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, set, session: { idToken } }) => {
.delete("/repertoire/:repertoireId", async ({ db, params: { repertoireId }, set, session: { idToken, roles } }) => {
if (Option.isNone(idToken)) {
return error("Unauthorized", "Session invalid or expired");
}
if (!idToken.value.payload.roles.includes("Editor")) {
if (!roles.includes("Editor")) {
return error("Forbidden", "Must be an Editor");
}

View File

@@ -158,7 +158,7 @@ export const revalidateTokens = Effect.fn("revaildateTokens")(
userId: Option.map(it, ({ payload: { oid } }) => oid),
roles: Option.match(it, {
onNone: constant(Object.freeze([])),
onSome: ({ payload: { roles } }) => roles,
onSome: ({ payload: { roles } }) => roles ?? Object.freeze([]),
}),
});

View File

@@ -20,7 +20,7 @@ export interface IdTokenPayload {
readonly exp: number;
readonly name: string;
readonly oid: Common.UserId;
readonly roles: readonly string[];
readonly roles?: readonly string[];
}
const brandedString = <T>() => t.Transform(t.String())

View File

@@ -9,9 +9,9 @@ export interface User {
}
export interface SystemInformation {
readonly createdBy: Option.Option<User>;
readonly createdBy: Option.Option<User | null>;
readonly createdAt: string;
readonly modifiedBy: Option.Option<User>;
readonly modifiedBy: Option.Option<User | null>;
readonly modifiedAt: Option.Option<string>;
}
@@ -113,7 +113,8 @@ const RepertoireSemaphore = Effect.unsafeMakeSemaphore(1);
export const userLookup = (userId: UserId) => pipe(
Effect.promise((signal) => client.user({ userId }).get({ fetch: { signal } })),
Effect.flatMap(mapResponse),
Effect.map((x): User => x), // safely coerce to interface
Effect.catchAll((error) => error.status === 404 ? Effect.succeed(null) : Effect.fail(error)),
Effect.map((x): User | null => x), // safely coerce to interface
UserSemaphore.withPermits(1),
);

View File

@@ -1,6 +1,7 @@
import { client } from "@/client";
import { useLoading } from "@/hooks/useLoading.ts";
import { AttachmentId, PieceId } from "common";
import { Match } from "effect";
import JSZip from "jszip";
import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
import { useCallback, useEffect, useRef } from "react";
@@ -102,7 +103,13 @@ export default function Attachment() {
if (error !== null) {
return (
<div className="w-full h-full overflow-hidden flex items-center justify-center">
<div>Wystąpił błąd: {error.status === 422 ? error.value.message : error.value}</div>
<div>
Wystąpił błąd: {Match.value(error).pipe(
Match.when({ status: 422 }, ({ value }) => value.message),
Match.when({ status: 404 }, () => "Załącznik nie istnieje"),
Match.exhaustive,
)}
</div>
</div>
);
}

View File

@@ -38,7 +38,7 @@ export function Piece() {
return (
<div className="p-4 overflow-y-auto flex flex-wrap items-start gap-4">
{error !== null ? (
Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`
Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error.value)}`
) : (<>
<div className="flex flex-col gap-4 p-4 border rounded">
<h3 className="font-bold text-lg">Utwór</h3>

View File

@@ -8,7 +8,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { useLoadingEffect } from "@/hooks/useLoading";
import { authors, created, DEBOUNCE, modified } from "@/snippets";
import { PieceId } from "common";
import { Cause, Effect } from "effect";
import { Cause, Effect, Match } from "effect";
import { Loader2, Plus } from "lucide-react";
import { FormEventHandler, useId, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
@@ -91,7 +91,7 @@ export function Pieces() {
) : error !== null ? (
<TableRow>
<TableCell colSpan={4} className="text-center">
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`}
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error.value)}`}
</TableCell>
</TableRow>
) : (
@@ -124,7 +124,14 @@ function PieceRow(props: PieceRow.Props) {
if (error !== null) {
return (
<TableRow>
<TableCell colSpan={4}>Wystąpił błąd: {error.status === 422 ? error.value.message : error.value}</TableCell>
<TableCell colSpan={4}>
Wystąpił błąd: {Match.value(error).pipe(
Match.when({ status: 401 }, () => "Zaloguj się ponownie"),
Match.when({ status: 422 }, ({ value }) => value.message),
Match.when({ status: 404 }, () => "Utwór nie istnieje"),
Match.exhaustive,
)}
</TableCell>
</TableRow>
);
}

View File

@@ -9,7 +9,7 @@ import { useLoadingEffect } from "@/hooks/useLoading";
import { mapProp, Update, Updater } from "@/hooks/useStore";
import { authors, DEBOUNCE, saveDelay } from "@/snippets";
import { PieceId, RepertoireId } from "common";
import { Array, Cause, Effect, Option, pipe } from "effect";
import { Array, Cause, Effect, Match, Option, pipe } from "effect";
import { ChevronDown, ChevronUp, CircleMinus, Loader2, Plus } from "lucide-react";
import { FormEventHandler, useCallback, useId, useMemo, useRef, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
@@ -36,7 +36,7 @@ export function Repertoire() {
return (
<div className="p-4 overflow-y-auto flex flex-wrap items-start gap-4">
{error !== null ? (
Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`
Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error.value)}`
) : (<>
<div className="flex flex-col gap-4 p-4 border rounded">
<h3 className="font-bold">Repertuar</h3>
@@ -343,7 +343,7 @@ function AddEntryDialogContent(props: AddEntryDialogContent.Props) {
) : error !== null ? (
<TableRow>
<TableCell colSpan={4} className="text-center">
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`}
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error.value)}`}
</TableCell>
</TableRow>
) : (
@@ -410,7 +410,14 @@ function EntryDialogPieceRow(props: EntryDialogPieceRow.Props) {
if (error !== null) {
return (
<TableRow>
<TableCell colSpan={2}>Wystąpił błąd: {error.status === 422 ? error.value.message : error.value}</TableCell>
<TableCell colSpan={2}>
Wystąpił błąd: {Match.value(error).pipe(
Match.when({ status: 401 }, () => "Zaloguj się ponownie"),
Match.when({ status: 422 }, ({ value }) => value.message),
Match.when({ status: 404 }, () => "Utwór nie istnieje"),
Match.exhaustive,
)}
</TableCell>
</TableRow>
);
}

View File

@@ -8,7 +8,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { useLoadingEffect } from "@/hooks/useLoading";
import { created, DEBOUNCE, modified } from "@/snippets";
import { RepertoireId } from "common";
import { Cause, Effect } from "effect";
import { Cause, Effect, Match } from "effect";
import { Loader2, Plus } from "lucide-react";
import { FormEventHandler, ReactNode, useId, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
@@ -79,7 +79,7 @@ export function Repertoires() {
) : error !== null ? (
<TableRow>
<TableCell colSpan={4} className="text-center">
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`}
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error.value)}`}
</TableCell>
</TableRow>
) : (
@@ -112,7 +112,14 @@ function RepertoireRow(props: RepertoireRow.Props) {
if (error !== null) {
return (
<TableRow>
<TableCell colSpan={4}>Wystąpił błąd: {error.status === 422 ? error.value.message : error.value}</TableCell>
<TableCell colSpan={4}>
Wystąpił błąd: {Match.value(error).pipe(
Match.when({ status: 401 }, () => "Zaloguj się ponownie"),
Match.when({ status: 422 }, ({ value }) => value.message),
Match.when({ status: 404 }, () => "Repertuar nie istnieje"),
Match.exhaustive,
)}
</TableCell>
</TableRow>
);
}

View File

@@ -36,7 +36,11 @@ export function created({ createdAt, createdBy }: SystemInformation): ReactNode
if (Option.isSome(createdBy)) {
nodes.push(<br />);
nodes.push(`przez ${createdBy.value.displayName}`);
if (createdBy.value !== null) {
nodes.push(`przez ${createdBy.value.displayName}`);
} else {
nodes.push("przez nieznanego użytkownika");
}
}
return nodes;
@@ -48,7 +52,11 @@ export function modified({ modifiedAt, modifiedBy }: SystemInformation): ReactNo
if (Option.isNone(modifiedBy)) {
return "\u2014";
} else {
return `przez ${modifiedBy.value.displayName}`;
if (modifiedBy.value !== null) {
return `przez ${modifiedBy.value.displayName}`;
} else {
return "przez nieznanego użytkownika";
}
}
}
@@ -56,7 +64,11 @@ export function modified({ modifiedAt, modifiedBy }: SystemInformation): ReactNo
if (Option.isSome(modifiedBy)) {
nodes.push(<br />);
nodes.push(`przez ${modifiedBy.value.displayName}`);
if (modifiedBy.value !== null) {
nodes.push(`przez ${modifiedBy.value.displayName}`);
} else {
nodes.push("przez nieznanego użytkownika");
}
}
return nodes;