diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index b2223da..74cfab4 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -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"); } diff --git a/packages/backend/src/auth.ts b/packages/backend/src/auth.ts index fb261b4..277b894 100644 --- a/packages/backend/src/auth.ts +++ b/packages/backend/src/auth.ts @@ -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([]), }), }); diff --git a/packages/backend/src/model.ts b/packages/backend/src/model.ts index 1dac204..9ac5bb0 100644 --- a/packages/backend/src/model.ts +++ b/packages/backend/src/model.ts @@ -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.Transform(t.String()) diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 1c165b7..204009d 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -9,9 +9,9 @@ export interface User { } export interface SystemInformation { - readonly createdBy: Option.Option; + readonly createdBy: Option.Option; readonly createdAt: string; - readonly modifiedBy: Option.Option; + readonly modifiedBy: Option.Option; readonly modifiedAt: Option.Option; } @@ -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), ); diff --git a/packages/frontend/src/routes/Attachment.tsx b/packages/frontend/src/routes/Attachment.tsx index 2aae05d..e65fbc1 100644 --- a/packages/frontend/src/routes/Attachment.tsx +++ b/packages/frontend/src/routes/Attachment.tsx @@ -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 (
-
Wystąpił błąd: {error.status === 422 ? error.value.message : error.value}
+
+ 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, + )} +
); } diff --git a/packages/frontend/src/routes/Piece.tsx b/packages/frontend/src/routes/Piece.tsx index 73a2ae4..ba60039 100644 --- a/packages/frontend/src/routes/Piece.tsx +++ b/packages/frontend/src/routes/Piece.tsx @@ -38,7 +38,7 @@ export function Piece() { return (
{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)}` ) : (<>

Utwór

diff --git a/packages/frontend/src/routes/Pieces.tsx b/packages/frontend/src/routes/Pieces.tsx index 831d24b..60eaa93 100644 --- a/packages/frontend/src/routes/Pieces.tsx +++ b/packages/frontend/src/routes/Pieces.tsx @@ -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 ? ( - {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)}`} ) : ( @@ -124,7 +124,14 @@ function PieceRow(props: PieceRow.Props) { if (error !== null) { return ( - Wystąpił błąd: {error.status === 422 ? error.value.message : error.value} + + 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, + )} + ); } diff --git a/packages/frontend/src/routes/Repertoire.tsx b/packages/frontend/src/routes/Repertoire.tsx index 972afde..159b4ee 100644 --- a/packages/frontend/src/routes/Repertoire.tsx +++ b/packages/frontend/src/routes/Repertoire.tsx @@ -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 (
{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)}` ) : (<>

Repertuar

@@ -343,7 +343,7 @@ function AddEntryDialogContent(props: AddEntryDialogContent.Props) { ) : 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)}`} ) : ( @@ -410,7 +410,14 @@ function EntryDialogPieceRow(props: EntryDialogPieceRow.Props) { if (error !== null) { return ( - Wystąpił błąd: {error.status === 422 ? error.value.message : error.value} + + 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, + )} + ); } diff --git a/packages/frontend/src/routes/Repertoires.tsx b/packages/frontend/src/routes/Repertoires.tsx index 3bf63e1..0f3bad5 100644 --- a/packages/frontend/src/routes/Repertoires.tsx +++ b/packages/frontend/src/routes/Repertoires.tsx @@ -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 ? ( - {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)}`} ) : ( @@ -112,7 +112,14 @@ function RepertoireRow(props: RepertoireRow.Props) { if (error !== null) { return ( - Wystąpił błąd: {error.status === 422 ? error.value.message : error.value} + + 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, + )} + ); } diff --git a/packages/frontend/src/snippets.tsx b/packages/frontend/src/snippets.tsx index f1ea2f6..a631767 100644 --- a/packages/frontend/src/snippets.tsx +++ b/packages/frontend/src/snippets.tsx @@ -36,7 +36,11 @@ export function created({ createdAt, createdBy }: SystemInformation): ReactNode if (Option.isSome(createdBy)) { nodes.push(
); - 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(
); - 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;