Changed error boundary to cover both full hub and disaptch app

This commit is contained in:
PxlLoewe
2025-05-29 22:25:30 -07:00
parent d968507484
commit 0cebe2b97e
24 changed files with 226 additions and 194 deletions

View File

@@ -0,0 +1,45 @@
"use client";
import { useEffect } from "react";
export const Error = ({ statusCode, title }: { statusCode: number; title: string }) => {
return (
<div className="flex-1 flex items-center justify-center h-full">
<div className="rounded-2xl bg-base-300 p-8 text-center max-w-md w-full">
<h1 className="text-6xl font-bold text-red-500">{statusCode}</h1>
<p className="text-xl font-semibold mt-4">Oh nein! Ein Fehler ist aufgetreten.</p>
<p className="text-gray-600 mt-2">{title || "Ein unerwarteter Fehler ist aufgetreten."}</p>
<button onClick={() => window.location.reload()} className="btn btn-dash my-2">
Refresh Page
</button>
</div>
</div>
);
};
export const ErrorFallback = ({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) => {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
);
};

View File

@@ -0,0 +1,29 @@
"use client";
import { ErrorBoundary } from "react-error-boundary";
import { Error as ErrorComp } from "./Error";
export const CustomErrorBoundary = ({ children }: { children?: React.ReactNode }) => {
return (
<ErrorBoundary
fallbackRender={({ error }) => {
console.log(error);
let errorTest;
let errorCode = 500;
if ("statusCode" in error) {
errorCode = (error as any).statusCode;
}
if ("message" in error || error instanceof Error) {
errorTest = (error as any).message;
} else if (typeof error === "string") {
errorTest = error;
} else {
errorTest = "Ein unerwarteter Fehler ist aufgetreten.";
}
return <ErrorComp title={errorTest} statusCode={errorCode} />;
}}
>
{children}
</ErrorBoundary>
);
};

View File

@@ -14,7 +14,6 @@ import { Map as TMap } from "leaflet";
const Map = () => { const Map = () => {
const ref = useRef<TMap | null>(null); const ref = useRef<TMap | null>(null);
const { map, setMap } = useMapStore(); const { map, setMap } = useMapStore();
useEffect(() => { useEffect(() => {
// Sync map zoom and center with the map store // Sync map zoom and center with the map store
if (ref.current) { if (ref.current) {

View File

@@ -70,9 +70,25 @@ export const options: AuthOptions = {
return token; return token;
}, },
session: async ({ session, user, token }) => { session: async ({ session, user, token }) => {
const dbUser = await prisma.user.findUnique({
where: {
id: token?.sub,
},
});
if (!dbUser) {
return {
...session,
user: {
name: null,
email: null,
image: null,
},
expires: new Date().toISOString(),
};
}
return { return {
...session, ...session,
user: token, user: dbUser,
}; };
}, },
}, },

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import Navbar from "./_components/navbar/Navbar"; import Navbar from "./_components/navbar/Navbar";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getServerSession } from "../api/auth/[...nextauth]/auth"; import { getServerSession } from "../api/auth/[...nextauth]/auth";
import { Error } from "_components/Error";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "VAR v2: Disponent", title: "VAR v2: Disponent",
@@ -14,9 +15,12 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const session = await getServerSession(); const session = await getServerSession();
if (!session) { if (!session) {
redirect("/login"); redirect("/login");
} }
if (!session.user.permissions.includes("DISPO"))
return <Error title="Zugriff verweigert" statusCode={403} />;
return ( return (
<> <>
<Navbar /> <Navbar />

View File

@@ -5,6 +5,9 @@ import { NextAuthSessionProvider } from "./_components/AuthSessionProvider";
import { getServerSession } from "./api/auth/[...nextauth]/auth"; import { getServerSession } from "./api/auth/[...nextauth]/auth";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { QueryProvider } from "_components/QueryProvider"; import { QueryProvider } from "_components/QueryProvider";
import { Error as ErrorComp } from "_components/Error";
import { ErrorBoundary } from "react-error-boundary";
import { CustomErrorBoundary } from "_components/ErrorBoundary";
const geistSans = localFont({ const geistSans = localFont({
src: "./fonts/GeistVF.woff", src: "./fonts/GeistVF.woff",
@@ -47,9 +50,7 @@ export default async function RootLayout({
reverseOrder={false} reverseOrder={false}
/> />
<QueryProvider> <QueryProvider>
<NextAuthSessionProvider session={session}> <NextAuthSessionProvider session={session}>{children}</NextAuthSessionProvider>
{children}
</NextAuthSessionProvider>
</QueryProvider> </QueryProvider>
</body> </body>
</html> </html>

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import Navbar from "./_components/navbar/Navbar"; import Navbar from "./_components/navbar/Navbar";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getServerSession } from "../api/auth/[...nextauth]/auth"; import { getServerSession } from "../api/auth/[...nextauth]/auth";
import { Error } from "_components/Error";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "VAR v2: Pilot", title: "VAR v2: Pilot",
@@ -14,9 +15,12 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const session = await getServerSession(); const session = await getServerSession();
if (!session) { if (!session) {
redirect("/login"); redirect("/login");
} }
if (!session.user.permissions.includes("PILOT"))
return <Error title="Zugriff verweigert" statusCode={403} />;
return ( return (
<> <>
<Navbar /> <Navbar />

View File

@@ -36,6 +36,7 @@
"postcss": "^8.5.1", "postcss": "^8.5.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-error-boundary": "^5.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-leaflet": "^5.0.0-rc.2", "react-leaflet": "^5.0.0-rc.2",

View File

@@ -78,16 +78,12 @@ const Template = ({ user, password }: { user: User; password: string }) => (
<tbody> <tbody>
<tr> <tr>
<td> <td>
<table <table align="center" width="680" style={{ margin: "0 auto", color: "#000000" }}>
align="center"
width="680"
style={{ margin: "0 auto", color: "#000000" }}
>
<tbody> <tbody>
<tr> <tr>
<td style={{ textAlign: "center", paddingTop: "30px" }}> <td style={{ textAlign: "center", paddingTop: "30px" }}>
<img <img
src={`${process.env.HUB_URL}/mail/var_logo.png`} src={`${process.env.NEXT_PUBLIC_HUB_URL}/mail/var_logo.png`}
alt="Logo" alt="Logo"
width="80" width="80"
style={{ display: "block", margin: "0 auto" }} style={{ display: "block", margin: "0 auto" }}
@@ -125,9 +121,8 @@ const Template = ({ user, password }: { user: User; password: string }) => (
padding: "20px", padding: "20px",
}} }}
> >
Dein Passwort wurde erfolgreich geändert. Wenn du diese Dein Passwort wurde erfolgreich geändert. Wenn du diese Änderung nicht
Änderung nicht vorgenommen hast, kontaktiere bitte sofort vorgenommen hast, kontaktiere bitte sofort unseren Support.
unseren Support.
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -191,12 +186,6 @@ const Template = ({ user, password }: { user: User; password: string }) => (
</Html> </Html>
); );
export function renderPasswordChanged({ export function renderPasswordChanged({ user, password }: { user: User; password: string }) {
user,
password,
}: {
user: User;
password: string;
}) {
return render(<Template user={user} password={password} />); return render(<Template user={user} password={password} />);
} }

View File

@@ -6,12 +6,11 @@ import { eventCompleted } from "../../../helper/events";
const page = async () => { const page = async () => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return null; if (!session) return null;
const user = await prisma.user.findUnique({
where: { const user = session.user;
id: session.user.id,
},
});
if (!user) return null; if (!user) return null;
const events = await prisma.event.findMany({ const events = await prisma.event.findMany({

View File

@@ -7,10 +7,7 @@ import { PlaneIcon } from "lucide-react";
export const PilotStats = async () => { export const PilotStats = async () => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return null; if (!session) return null;
const user = await prisma.user.findUnique({ const user = session.user;
where: { id: session.user.id },
});
const mostFlownStationsIds = await prisma.missionOnStationUsers.groupBy({ const mostFlownStationsIds = await prisma.missionOnStationUsers.groupBy({
where: { where: {
userId: user?.id, userId: user?.id,
@@ -63,8 +60,7 @@ export const PilotStats = async () => {
orderBy: { _count: { userId: "desc" } }, orderBy: { _count: { userId: "desc" } },
}); });
const ownRankMissionsFlown = const ownRankMissionsFlown = missionsFlownRanks.findIndex((rank) => rank.userId === user?.id) + 1;
missionsFlownRanks.findIndex((rank) => rank.userId === user?.id) + 1;
const totalUserCount = await prisma.user.count({ const totalUserCount = await prisma.user.count({
where: { where: {
@@ -108,8 +104,7 @@ export const PilotStats = async () => {
<div className="stat-title">Einsätze geflogen</div> <div className="stat-title">Einsätze geflogen</div>
<div className="stat-value text-primary">{totalFlownMissions}</div> <div className="stat-value text-primary">{totalFlownMissions}</div>
<div className="stat-desc"> <div className="stat-desc">
Du bist damit unter den top{" "} Du bist damit unter den top {((ownRankMissionsFlown * 100) / totalUserCount).toFixed(0)}%!
{((ownRankMissionsFlown * 100) / totalUserCount).toFixed(0)}%!
</div> </div>
</div> </div>
@@ -140,17 +135,12 @@ export const PilotStats = async () => {
<div className="stat-figure text-info"> <div className="stat-figure text-info">
<PlaneIcon className="w-8 h-8" /> <PlaneIcon className="w-8 h-8" />
</div> </div>
<div className="stat-value text-info"> <div className="stat-value text-info">{mostFlownStation?.bosCallsign}</div>
{mostFlownStation?.bosCallsign} <div className="stat-title">War bisher dein Rettungsmittel der Wahl</div>
</div>
<div className="stat-title">
War bisher dein Rettungsmittel der Wahl
</div>
{unflownStationsCount > 0 && ( {unflownStationsCount > 0 && (
<div className="stat-desc text-secondary"> <div className="stat-desc text-secondary">
{unflownStationsCount}{" "} {unflownStationsCount} {unflownStationsCount > 1 ? "Stationen" : "Station"} warten
{unflownStationsCount > 1 ? "Stationen" : "Station"} warten noch noch auf dich!
auf dich!
</div> </div>
)} )}
{unflownStationsCount === 0 && ( {unflownStationsCount === 0 && (
@@ -167,9 +157,7 @@ export const PilotStats = async () => {
export const DispoStats = async () => { export const DispoStats = async () => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return null; if (!session) return null;
const user = await prisma.user.findUnique({ const user = session.user;
where: { id: session.user.id },
});
const dispoSessions = await prisma.connectedDispatcher.findMany({ const dispoSessions = await prisma.connectedDispatcher.findMany({
where: { where: {
@@ -275,9 +263,7 @@ export const DispoStats = async () => {
<div className="stat-figure text-info"> <div className="stat-figure text-info">
<PlaneIcon className="w-8 h-8" /> <PlaneIcon className="w-8 h-8" />
</div> </div>
<div className="stat-value text-info"> <div className="stat-value text-info">{mostDispatchedStation?.bosCallsign}</div>
{mostDispatchedStation?.bosCallsign}
</div>
<div className="stat-title">Wurde von dir am meisten Disponiert</div> <div className="stat-title">Wurde von dir am meisten Disponiert</div>
<div className="stat-desc text-secondary"> <div className="stat-desc text-secondary">
{mostDispatchedStationIds[0]?._count.missionStationIds} Einsätze {mostDispatchedStationIds[0]?._count.missionStationIds} Einsätze

View File

@@ -1,20 +1,18 @@
import { prisma } from "@repo/db";
import { Error } from "_components/Error"; import { Error } from "_components/Error";
import { getServerSession } from "api/auth/[...nextauth]/auth"; import { getServerSession } from "api/auth/[...nextauth]/auth";
export default async ({ children }: { children: React.ReactNode }) => { const AdminEventLayout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />; if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />;
const user = await prisma.user.findUnique({ const user = session.user;
where: {
id: session.user.id,
},
});
if (!user?.permissions.includes("ADMIN_EVENT")) if (!user?.permissions.includes("ADMIN_EVENT"))
return <Error title="Keine Berechtigung" statusCode={403} />; return <Error title="Keine Berechtigung" statusCode={403} />;
return <>{children}</>; return <>{children}</>;
}; };
AdminEventLayout.displayName = "AdminEventLayout";
export default AdminEventLayout;

View File

@@ -1,20 +1,19 @@
import { prisma } from "@repo/db";
import { Error } from "_components/Error"; import { Error } from "_components/Error";
import { getServerSession } from "api/auth/[...nextauth]/auth"; import { getServerSession } from "api/auth/[...nextauth]/auth";
export default async ({ children }: { children: React.ReactNode }) => { const AdminKeywordLayout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />; if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />;
const user = await prisma.user.findUnique({ const user = session.user;
where: {
id: session.user.id,
},
});
if (!user?.permissions.includes("ADMIN_KEYWORD")) if (!user?.permissions.includes("ADMIN_KEYWORD"))
return <Error title="Keine Berechtigung" statusCode={403} />; return <Error title="Keine Berechtigung" statusCode={403} />;
return <>{children}</>; return <>{children}</>;
}; };
AdminKeywordLayout.displayName = "AdminKeywordLayout";
export default AdminKeywordLayout;

View File

@@ -1,21 +1,12 @@
import { prisma } from "@repo/db";
import { Error } from "_components/Error"; import { Error } from "_components/Error";
import { getServerSession } from "api/auth/[...nextauth]/auth"; import { getServerSession } from "api/auth/[...nextauth]/auth";
export default async function ReportLayout({ export default async function ReportLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />; if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />;
const user = await prisma.user.findUnique({ const user = session.user;
where: {
id: session.user.id,
},
});
if (!user?.permissions.includes("ADMIN_EVENT")) if (!user?.permissions.includes("ADMIN_EVENT"))
return <Error title="Keine Berechtigung" statusCode={403} />; return <Error title="Keine Berechtigung" statusCode={403} />;

View File

@@ -1,20 +1,19 @@
import { prisma } from "@repo/db";
import { Error } from "_components/Error"; import { Error } from "_components/Error";
import { getServerSession } from "api/auth/[...nextauth]/auth"; import { getServerSession } from "api/auth/[...nextauth]/auth";
export default async ({ children }: { children: React.ReactNode }) => { const AdminStationLayout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />; if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />;
const user = await prisma.user.findUnique({ const user = session.user;
where: {
id: session.user.id,
},
});
if (!user?.permissions.includes("ADMIN_STATION")) if (!user?.permissions.includes("ADMIN_STATION"))
return <Error title="Keine Berechtigung" statusCode={403} />; return <Error title="Keine Berechtigung" statusCode={403} />;
return <>{children}</>; return <>{children}</>;
}; };
AdminStationLayout.displayName = "AdminStationLayout";
export default AdminStationLayout;

View File

@@ -11,12 +11,7 @@ import {
} from "@repo/db"; } from "@repo/db";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { import { deleteDispoHistory, deletePilotHistory, editUser, resetPassword } from "../../action";
deleteDispoHistory,
deletePilotHistory,
editUser,
resetPassword,
} from "../../action";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { import {
PersonIcon, PersonIcon,
@@ -42,9 +37,7 @@ interface ProfileFormProps {
user: User; user: User;
} }
export const ProfileForm: React.FC<ProfileFormProps> = ({ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormProps) => {
user,
}: ProfileFormProps) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const form = useForm<User>({ const form = useForm<User>({
defaultValues: user, defaultValues: user,
@@ -83,9 +76,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
/> />
</label> </label>
{form.formState.errors.firstname && ( {form.formState.errors.firstname && (
<p className="text-error"> <p className="text-error">{form.formState.errors.firstname.message}</p>
{form.formState.errors.firstname.message}
</p>
)} )}
<label className="floating-label w-full mb-5"> <label className="floating-label w-full mb-5">
<span className="text-lg flex items-center gap-2"> <span className="text-lg flex items-center gap-2">
@@ -100,9 +91,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
/> />
</label> </label>
{form.formState.errors.lastname && ( {form.formState.errors.lastname && (
<p className="text-error"> <p className="text-error">{form.formState.errors.lastname?.message}</p>
{form.formState.errors.lastname?.message}
</p>
)} )}
<label className="floating-label w-full"> <label className="floating-label w-full">
<span className="text-lg flex items-center gap-2"> <span className="text-lg flex items-center gap-2">
@@ -154,11 +143,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
); );
}; };
export const ConnectionHistory: React.FC<{ user: User }> = ({ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: User }) => {
user,
}: {
user: User;
}) => {
const dispoTableRef = useRef<PaginatedTableRef>(null); const dispoTableRef = useRef<PaginatedTableRef>(null);
return ( return (
<div className="card-body flex-row flex-wrap"> <div className="card-body flex-row flex-wrap">
@@ -185,9 +170,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
accessorKey: "loginTime", accessorKey: "loginTime",
header: "Login", header: "Login",
cell: ({ row }) => { cell: ({ row }) => {
return new Date(row.getValue("loginTime")).toLocaleString( return new Date(row.getValue("loginTime")).toLocaleString("de-DE");
"de-DE",
);
}, },
}, },
{ {
@@ -197,9 +180,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
return <span className="text-success">Online</span>; return <span className="text-success">Online</span>;
} }
const loginTime = new Date(row.original.loginTime).getTime(); const loginTime = new Date(row.original.loginTime).getTime();
const logoutTime = new Date( const logoutTime = new Date(row.original.logoutTime).getTime();
row.original.logoutTime,
).getTime();
const timeOnline = logoutTime - loginTime; const timeOnline = logoutTime - loginTime;
@@ -253,10 +234,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
header: "Station", header: "Station",
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<Link <Link className="link link-hover" href={`/admin/station/${row.original.id}`}>
className="link link-hover"
href={`/admin/station/${row.original.id}`}
>
{row.original.Station.bosCallsign} {row.original.Station.bosCallsign}
</Link> </Link>
); );
@@ -266,9 +244,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
accessorKey: "loginTime", accessorKey: "loginTime",
header: "Login", header: "Login",
cell: ({ row }) => { cell: ({ row }) => {
return new Date(row.getValue("loginTime")).toLocaleString( return new Date(row.getValue("loginTime")).toLocaleString("de-DE");
"de-DE",
);
}, },
}, },
{ {
@@ -278,9 +254,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
return <span className="text-success">Online</span>; return <span className="text-success">Online</span>;
} }
const loginTime = new Date(row.original.loginTime).getTime(); const loginTime = new Date(row.original.loginTime).getTime();
const logoutTime = new Date( const logoutTime = new Date(row.original.logoutTime).getTime();
row.original.logoutTime,
).getTime();
const timeOnline = logoutTime - loginTime; const timeOnline = logoutTime - loginTime;
const hours = Math.floor(timeOnline / 1000 / 60 / 60); const hours = Math.floor(timeOnline / 1000 / 60 / 60);
@@ -360,19 +334,10 @@ export const UserReports = ({ user }: { user: User }) => {
return `${user.firstname} ${user.lastname} (${user.publicId})`; return `${user.firstname} ${user.lastname} (${user.publicId})`;
}, },
}, },
{
accessorKey: "Reported",
header: "Reported",
cell: ({ row }) => {
const user = row.getValue("Reported") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{ {
accessorKey: "timestamp", accessorKey: "timestamp",
header: "Time", header: "Time",
cell: ({ row }) => cell: ({ row }) => new Date(row.getValue("timestamp")).toLocaleString(),
new Date(row.getValue("timestamp")).toLocaleString(),
}, },
{ {
accessorKey: "actions", accessorKey: "actions",
@@ -411,12 +376,7 @@ interface AdminFormProps {
}; };
} }
export const AdminForm = ({ export const AdminForm = ({ user, dispoTime, pilotTime, reports }: AdminFormProps) => {
user,
dispoTime,
pilotTime,
reports,
}: AdminFormProps) => {
const router = useRouter(); const router = useRouter();
return ( return (
@@ -495,8 +455,7 @@ export const AdminForm = ({
</div> </div>
<div className="stat-title">Dispo Zeit</div> <div className="stat-title">Dispo Zeit</div>
<div className="stat-desc text-secondary"> <div className="stat-desc text-secondary">
{dispoTime.lastLogin && {dispoTime.lastLogin && new Date(dispoTime.lastLogin).toLocaleString("de-DE")}
new Date(dispoTime.lastLogin).toLocaleString("de-DE")}
</div> </div>
</div> </div>
<div className="stat"> <div className="stat">
@@ -508,8 +467,7 @@ export const AdminForm = ({
</div> </div>
<div className="stat-title">Pilot Zeit</div> <div className="stat-title">Pilot Zeit</div>
<div className="stat-desc text-secondary"> <div className="stat-desc text-secondary">
{pilotTime.lastLogin && {pilotTime.lastLogin && new Date(pilotTime.lastLogin).toLocaleString("de-DE")}
new Date(pilotTime.lastLogin).toLocaleString("de-DE")}
</div> </div>
</div> </div>
</div> </div>
@@ -523,12 +481,7 @@ export const AdminForm = ({
<div className="stat-figure text-primary"> <div className="stat-figure text-primary">
<ExclamationTriangleIcon className="w-8 h-8" /> <ExclamationTriangleIcon className="w-8 h-8" />
</div> </div>
<div <div className={cn("stat-value text-primary", reports.open && "text-warning")}>
className={cn(
"stat-value text-primary",
reports.open && "text-warning",
)}
>
{reports.open} {reports.open}
</div> </div>
<div className="stat-title">Offen</div> <div className="stat-title">Offen</div>
@@ -539,9 +492,7 @@ export const AdminForm = ({
</div> </div>
<div className="stat-value text-primary">{reports.total60Days}</div> <div className="stat-value text-primary">{reports.total60Days}</div>
<div className="stat-title">in den letzten 60 Tagen</div> <div className="stat-title">in den letzten 60 Tagen</div>
<div className="stat-desc text-secondary"> <div className="stat-desc text-secondary">{reports.total} insgesammt</div>
{reports.total} insgesammt
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,20 +1,19 @@
import { prisma } from "@repo/db";
import { Error } from "_components/Error"; import { Error } from "_components/Error";
import { getServerSession } from "api/auth/[...nextauth]/auth"; import { getServerSession } from "api/auth/[...nextauth]/auth";
export default async ({ children }: { children: React.ReactNode }) => { const AdminUserLayout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />; if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />;
const user = await prisma.user.findUnique({ const user = session.user;
where: {
id: session.user.id,
},
});
if (!user?.permissions.includes("ADMIN_USER")) if (!user?.permissions.includes("ADMIN_USER"))
return <Error title="Keine Berechtigung" statusCode={403} />; return <Error title="Keine Berechtigung" statusCode={403} />;
return <>{children}</>; return <>{children}</>;
}; };
AdminUserLayout.displayName = "AdminUserLayout";
export default AdminUserLayout;

View File

@@ -6,11 +6,7 @@ import { RocketIcon } from "@radix-ui/react-icons";
const page = async () => { const page = async () => {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return null; if (!session) return null;
const user = await prisma.user.findUnique({ const user = session.user;
where: {
id: session.user.id,
},
});
if (!user) return null; if (!user) return null;
const events = await prisma.event.findMany({ const events = await prisma.event.findMany({

View File

@@ -1,13 +1,6 @@
"use client"; "use client";
import { Error as ErrorComp } from "_components/Error";
import { ErrorBoundary } from "react-error-boundary";
import { NextPage } from "next";
const AuthLayout: NextPage< const AuthLayout = ({ children }: { children: React.ReactNode }) => (
Readonly<{
children: React.ReactNode;
}>
> = ({ children }) => (
<div <div
className="hero min-h-screen" className="hero min-h-screen"
style={{ style={{
@@ -16,30 +9,11 @@ const AuthLayout: NextPage<
> >
<div className="hero-overlay bg-neutral/60"></div> <div className="hero-overlay bg-neutral/60"></div>
<div className="hero-content text-center "> <div className="hero-content text-center ">
<ErrorBoundary <div className="max-w-lg">
fallbackRender={({ error, resetErrorBoundary }) => { <div className="card rounded-2xl bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
console.log(error); {children}
let errorTest;
let errorCode = 500;
if ("statusCode" in error) {
errorCode = error.statusCode;
}
if ("message" in error || error instanceof Error) {
errorTest = error.message;
} else if (typeof error === "string") {
errorTest = error;
} else {
errorTest = "Ein unerwarteter Fehler ist aufgetreten.";
}
return <ErrorComp title={errorTest} statusCode={errorCode} />;
}}
>
<div className="max-w-lg">
<div className="card rounded-2xl bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
{children}
</div>
</div> </div>
</ErrorBoundary> </div>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,29 @@
"use client";
import { ErrorBoundary } from "react-error-boundary";
import { Error as ErrorComp } from "./Error";
export const CustomErrorBoundary = ({ children }: { children?: React.ReactNode }) => {
return (
<ErrorBoundary
fallbackRender={({ error }) => {
console.log(error);
let errorTest;
let errorCode = 500;
if ("statusCode" in error) {
errorCode = (error as any).statusCode;
}
if ("message" in error || error instanceof Error) {
errorTest = (error as any).message;
} else if (typeof error === "string") {
errorTest = error;
} else {
errorTest = "Ein unerwarteter Fehler ist aufgetreten.";
}
return <ErrorComp title={errorTest} statusCode={errorCode} />;
}}
>
{children}
</ErrorBoundary>
);
};

View File

@@ -27,7 +27,7 @@ export const options: AuthOptions = {
}, },
}), }),
], ],
secret: process.env.AUTH_HUB_SECRET, secret: process.env.AUTH_HUB_SECRET,
session: { session: {
strategy: "jwt", strategy: "jwt",
@@ -62,10 +62,26 @@ export const options: AuthOptions = {
} }
return token; return token;
}, },
session: async ({ session, user, token }) => { session: async ({ session, token }) => {
const dbUser = await prisma.user.findUnique({
where: {
id: token?.sub,
},
});
if (!dbUser) {
return {
...session,
user: {
name: null,
email: null,
image: null,
},
expires: new Date().toISOString(),
};
}
return { return {
...session, ...session,
user: token, user: dbUser,
}; };
}, },
}, },

View File

@@ -2,6 +2,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import { NextAuthSessionProvider } from "./_components/AuthSessionProvider"; import { NextAuthSessionProvider } from "./_components/AuthSessionProvider";
import { getServerSession } from "./api/auth/[...nextauth]/auth"; import { getServerSession } from "./api/auth/[...nextauth]/auth";
import "./globals.css"; import "./globals.css";
import { CustomErrorBoundary } from "_components/ErrorBoundary";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -24,7 +25,7 @@ const RootLayout = async ({
<html lang="en"> <html lang="en">
<NextAuthSessionProvider session={session}> <NextAuthSessionProvider session={session}>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children} <CustomErrorBoundary>{children}</CustomErrorBoundary>
</body> </body>
</NextAuthSessionProvider> </NextAuthSessionProvider>
</html> </html>

View File

@@ -3,7 +3,7 @@ export const services = [
id: "1", id: "1",
service: "dispatch", service: "dispatch",
name: "Leitstellendisposition", name: "Leitstellendisposition",
approvedUrls: ["https://dispatch.premiumag.de"], approvedUrls: [process.env.NEXT_PUBLIC_DISPATCH_URL],
}, },
{ {
id: "2", id: "2",

6
pnpm-lock.yaml generated
View File

@@ -92,6 +92,9 @@ importers:
react-dom: react-dom:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
react-error-boundary:
specifier: ^5.0.0
version: 5.0.0(react@19.1.0)
react-hook-form: react-hook-form:
specifier: ^7.54.2 specifier: ^7.54.2
version: 7.56.4(react@19.1.0) version: 7.56.4(react@19.1.0)
@@ -195,6 +198,9 @@ importers:
socket.io: socket.io:
specifier: ^4.8.1 specifier: ^4.8.1
version: 4.8.1 version: 4.8.1
tsx:
specifier: ^4.19.4
version: 4.19.4
devDependencies: devDependencies:
'@repo/db': '@repo/db':
specifier: '*' specifier: '*'