-
-
+
{event.finishedBadges.map((b) => {
return ;
})}
-
+
-
+
Teilnahmevoraussetzungen:
{!event.requiredBadges.length && "Keine"}
{!!event.requiredBadges.length && (
-
-
Abzeichen:
+
+
Abzeichen:
{event.requiredBadges.map((badge) => (
@@ -71,11 +64,9 @@ export const EventCard = ({
)}
diff --git a/apps/hub/app/(app)/events/_components/Modal.tsx b/apps/hub/app/(app)/events/_components/Modal.tsx
index bcf0e910..74b45364 100644
--- a/apps/hub/app/(app)/events/_components/Modal.tsx
+++ b/apps/hub/app/(app)/events/_components/Modal.tsx
@@ -1,56 +1,30 @@
"use client";
import { useEffect } from "react";
import { CheckCircledIcon, EnterIcon, DrawingPinFilledIcon } from "@radix-ui/react-icons";
-import { Event, EventAppointment, Participant, User } from "@repo/db";
+import { Event, Participant, User } from "@repo/db";
import { cn } from "@repo/shared-components";
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
import {
BookCheck,
- Calendar,
Check,
CirclePlay,
- Clock10Icon,
ExternalLink,
EyeIcon,
Info,
TriangleAlert,
} from "lucide-react";
-import { useForm } from "react-hook-form";
-import {
- InputJsonValueType,
- ParticipantOptionalDefaults,
- ParticipantOptionalDefaultsSchema,
-} from "@repo/db/zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Select } from "../../../_components/ui/Select";
-import { useRouter } from "next/navigation";
-import { handleParticipantEnrolled } from "../../../../helper/events";
import { eventCompleted } from "@repo/shared-components";
import MDEditor from "@uiw/react-md-editor";
-import toast from "react-hot-toast";
-import { formatDate } from "date-fns";
interface ModalBtnProps {
title: string;
event: Event;
- dates: (EventAppointment & {
- Participants: { userId: string }[];
- })[];
- selectedAppointments: EventAppointment[];
participant?: Participant;
user: User;
modalId: string;
}
-const ModalBtn = ({
- title,
- dates,
- modalId,
- participant,
- selectedAppointments,
- event,
- user,
-}: ModalBtnProps) => {
+const ModalBtn = ({ title, modalId, participant, event, user }: ModalBtnProps) => {
useEffect(() => {
const modal = document.getElementById(modalId) as HTMLDialogElement;
const handleOpen = () => {
@@ -66,12 +40,6 @@ const ModalBtn = ({
modal?.removeEventListener("close", handleClose);
};
}, [modalId]);
- const router = useRouter();
-
- const canSelectDate =
- event.hasPresenceEvents &&
- !participant?.attended &&
- (selectedAppointments.length === 0 || participant?.appointmentCancelled);
const openModal = () => {
const modal = document.getElementById(modalId) as HTMLDialogElement;
@@ -82,29 +50,6 @@ const ModalBtn = ({
const modal = document.getElementById(modalId) as HTMLDialogElement;
modal?.close();
};
- const selectAppointmentForm = useForm
({
- resolver: zodResolver(ParticipantOptionalDefaultsSchema),
- defaultValues: {
- eventId: event.id,
- userId: user.id,
- ...participant,
- },
- });
- const selectedAppointment = selectedAppointments[0];
- const selectedDate = dates.find(
- (date) =>
- date.id === selectAppointmentForm.watch("eventAppointmentId") || selectedAppointment?.id,
- );
- const ownIndexInParticipantList = selectedDate?.Participants?.findIndex(
- (p) => p.userId === user.id,
- );
-
- const ownPlaceInParticipantList =
- typeof ownIndexInParticipantList === "number"
- ? ownIndexInParticipantList === -1
- ? (selectedDate?.Participants?.length ?? 0) + 1
- : ownIndexInParticipantList + 1
- : undefined;
const missingRequirements =
event.requiredBadges?.length > 0 &&
@@ -163,79 +108,6 @@ const ModalBtn = ({
/>
- {event.hasPresenceEvents && (
-
-
- Termine
-
-
- {!!dates.length && !selectedDate && (
- <>
-
Melde dich zu einem Termin an
-
({
- label: `${formatDate(date.appointmentDate, "dd.MM.yyyy HH:mm")} - (${date.Participants.length}/${event.maxParticipants})`,
- value: date.id,
- }))}
- name="eventAppointmentId"
- label={""}
- placeholder="Wähle einen Termin"
- className="min-w-[250px]"
- />
- >
- )}
- {selectedAppointment && !participant?.appointmentCancelled && (
-
-
Dein ausgewählter Termin (Deutsche Zeit)
-
-
- {new Date(selectedAppointment.appointmentDate).toLocaleString("de-DE", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- })}
-
-
- {participant?.attended ? (
-
-
- Du hast an dem Presenztermin teilgenommen
-
- ) : (
-
- Bitte erscheine ~5 minuten vor dem Termin im Discord
-
- )}
-
- )}
-
- {!dates.length && (
- Aktuell sind keine Termine verfügbar
- )}
-
- {!!selectedDate &&
- !!event.maxParticipants &&
- !!ownPlaceInParticipantList &&
- !!(ownPlaceInParticipantList > event.maxParticipants) && (
-
-
- Dieser Termin ist ausgebucht, wahrscheinlich wirst du nicht teilnehmen
- können. (Listenplatz: {ownPlaceInParticipantList} , max.{" "}
- {event.maxParticipants})
-
- )}
-
-
- )}
{event.finisherMoodleCourseId && (
@@ -261,64 +133,6 @@ const ModalBtn = ({
{!event.requiredBadges.length && "Keine"}
-
- {!!canSelectDate && (
- {
- const data = selectAppointmentForm.getValues();
- if (!data.eventAppointmentId) return;
-
- const participant = await upsertParticipant({
- ...data,
- enscriptionDate: new Date(),
- statusLog: data.statusLog?.filter((log) => log !== null),
- appointmentCancelled: false,
- });
- await handleParticipantEnrolled(participant.id.toString());
-
- router.refresh();
- closeModal();
- }}
- disabled={!selectAppointmentForm.watch("eventAppointmentId")}
- >
- Anmelden
-
- )}
- {selectedAppointment &&
- !participant?.appointmentCancelled &&
- !participant?.attended && (
- {
- await upsertParticipant({
- eventId: event.id,
- userId: participant!.userId,
- appointmentCancelled: true,
- statusLog: [
- ...(participant?.statusLog as unknown as InputJsonValueType[]),
- {
- data: {
- appointmentId: selectedAppointment.id,
- appointmentDate: selectedAppointment.appointmentDate,
- },
- user: `${user?.firstname} ${user?.lastname} - ${user?.publicId}`,
- event: "Termin abgesagt",
- timestamp: new Date(),
- },
- ],
- });
- toast.success("Termin abgesagt");
- router.refresh();
- }}
- className="btn btn-error btn-outline btn-wide"
- >
- Termin Absagen
-
- )}
-
@@ -335,7 +149,6 @@ const MoodleCourseIndicator = ({
completed,
moodleCourseId,
event,
- participant,
user,
}: {
user: User;
@@ -345,13 +158,6 @@ const MoodleCourseIndicator = ({
event: Event;
}) => {
const courseUrl = `${process.env.NEXT_PUBLIC_MOODLE_URL}/course/view.php?id=${moodleCourseId}`;
- if (event.hasPresenceEvents && !participant?.attended)
- return (
-
-
- Abschlusstest erst nach Teilnahme verfügbar
-
- );
if (completed)
return (
diff --git a/apps/hub/app/(app)/events/page.tsx b/apps/hub/app/(app)/events/page.tsx
index be2e660e..2aa8a479 100644
--- a/apps/hub/app/(app)/events/page.tsx
+++ b/apps/hub/app/(app)/events/page.tsx
@@ -10,63 +10,19 @@ const page = async () => {
if (!user) return null;
const events = await prisma.event.findMany({
+ orderBy: {
+ id: "desc",
+ },
where: {
hidden: false,
},
include: {
- Appointments: {
- where: {
- appointmentDate: {
- gte: new Date(),
- },
- },
- },
Participants: {
where: {
userId: user.id,
},
},
},
-
- orderBy: {
- id: "desc",
- },
- });
- const appointments = await prisma.eventAppointment.findMany({
- where: {
- appointmentDate: {
- gte: new Date(),
- },
- },
- include: {
- Participants: {
- select: {
- enscriptionDate: true,
- id: true,
- userId: true,
- },
- where: {
- appointmentCancelled: false,
- },
- orderBy: {
- enscriptionDate: "asc",
- },
- },
- _count: {
- select: {
- Participants: true,
- },
- },
- },
- });
- const userAppointments = await prisma.eventAppointment.findMany({
- where: {
- Participants: {
- some: {
- userId: user.id,
- },
- },
- },
});
return (
@@ -78,15 +34,7 @@ const page = async () => {
{events.map((event) => {
- return (
-
- );
+ return
;
})}
);
diff --git a/apps/hub/app/(app)/resources/page.tsx b/apps/hub/app/(app)/resources/page.tsx
index b8c60d98..1bfd5e87 100644
--- a/apps/hub/app/(app)/resources/page.tsx
+++ b/apps/hub/app/(app)/resources/page.tsx
@@ -11,7 +11,7 @@ export default function () {
image={Discord}
title="Discord Server"
BtnIcon={
}
- btnHref="https://discord.com/invite/x6FAMY7DW6"
+ btnHref="https://discord.gg/virtualairrescue"
btnLabel="Beitreten"
description="Tritt unserem Discordserver bei, um mit der Community in Kontakt zu bleiben, Unterstützung zu erhalten und über die neuesten Updates informiert zu werden. Wenn du beigetreten bist kannst du in deinen Einstellungen dein VAR-Konto mit deinem Discordkonto verknüpfen und eine Rolle zu erhalten.
"
diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx
index 64feabec..15399821 100644
--- a/apps/hub/app/(app)/settings/_components/forms.tsx
+++ b/apps/hub/app/(app)/settings/_components/forms.tsx
@@ -23,6 +23,7 @@ import toast from "react-hot-toast";
import { CircleAlert, Trash2 } from "lucide-react";
import { deleteUser, sendVerificationLink } from "(app)/admin/user/action";
import { setStandardName } from "../../../../helper/discord";
+import { logAction } from "(auth)/login/_components/action";
export const ProfileForm = ({
user,
@@ -101,6 +102,28 @@ export const ProfileForm = ({
userId: user.id,
});
}
+ if (user.firstname !== values.firstname) {
+ await logAction("PROFILE_CHANGE", {
+ field: "firstname",
+ oldValue: user.firstname,
+ newValue: values.firstname,
+ });
+ }
+ if (user.lastname !== values.lastname) {
+ await logAction("PROFILE_CHANGE", {
+ field: "lastname",
+ oldValue: user.lastname,
+ newValue: values.lastname,
+ });
+ }
+ if (user.email !== values.email) {
+ await logAction("PROFILE_CHANGE", {
+ field: "email",
+ oldValue: user.email,
+ newValue: values.email,
+ });
+ }
+
form.reset(values);
if (user.email !== values.email) {
await sendVerificationLink(user.id);
diff --git a/apps/hub/app/(auth)/login/_components/Login.tsx b/apps/hub/app/(auth)/login/_components/Login.tsx
index df02a2c2..f22db1d0 100644
--- a/apps/hub/app/(auth)/login/_components/Login.tsx
+++ b/apps/hub/app/(auth)/login/_components/Login.tsx
@@ -9,6 +9,7 @@ import { Toaster, toast } from "react-hot-toast";
import { z } from "zod";
import { Button } from "../../../_components/ui/Button";
import { useErrorBoundary } from "react-error-boundary";
+import { logAction } from "./action";
export const Login = () => {
const { showBoundary } = useErrorBoundary();
@@ -46,6 +47,10 @@ export const Login = () => {
});
return;
}
+
+ console.log("data", data);
+
+ await logAction("LOGIN");
redirect(searchParams.get("redirect") || "/");
} catch (error) {
showBoundary(error);
diff --git a/apps/hub/app/(auth)/login/_components/action.ts b/apps/hub/app/(auth)/login/_components/action.ts
new file mode 100644
index 00000000..155a1ebc
--- /dev/null
+++ b/apps/hub/app/(auth)/login/_components/action.ts
@@ -0,0 +1,87 @@
+"use server";
+import { LOG_TYPE, prisma } from "@repo/db";
+import { getServerSession } from "api/auth/[...nextauth]/auth";
+import { randomUUID } from "crypto";
+import { cookies, headers } from "next/headers";
+import { sendReportEmbed } from "../../../../helper/discord";
+
+export async function getOrSetDeviceId() {
+ const store = await cookies();
+ let deviceId = store.get("device_id")?.value;
+
+ if (!deviceId) {
+ deviceId = randomUUID();
+ store.set("device_id", deviceId, {
+ httpOnly: true,
+ secure: true,
+ sameSite: "lax",
+ path: "/",
+ maxAge: 60 * 60 * 24 * 365, // 1 Jahr
+ });
+ }
+
+ return deviceId;
+}
+
+export const logAction = async (
+ type: LOG_TYPE,
+ otherValues?: {
+ field?: string;
+ oldValue?: string;
+ newValue?: string;
+ userId?: string;
+ },
+) => {
+ const headersList = await headers();
+ const user = await getServerSession();
+
+ const ip =
+ headersList.get("X-Forwarded-For") ||
+ headersList.get("Forwarded") ||
+ headersList.get("X-Real-IP");
+
+ const deviceId = await getOrSetDeviceId();
+ if (type == "LOGIN" || type == "REGISTER") {
+ const existingLogs = await prisma.log.findMany({
+ where: {
+ type: "LOGIN",
+ userId: {
+ not: user?.user.id,
+ },
+ OR: [
+ {
+ ip: ip,
+ },
+ {
+ deviceId: deviceId,
+ },
+ ],
+ },
+ });
+ if (existingLogs.length > 0 && user?.user.id) {
+ // Möglicherweise ein doppelter Account, Report erstellen
+ const report = await prisma.report.create({
+ data: {
+ text: `Möglicher doppelter Account erkannt bei Login-Versuch.\n\nÜbereinstimmende Logs:\n${existingLogs
+ .map((log) => `- Log ID: ${log.id}, IP: ${log.ip}, Zeitstempel: ${log.timestamp}`)
+ .join("\n")}`,
+ reportedUserId: user?.user.id,
+ reportedUserRole: "LOGIN - Doppelter Account Verdacht",
+ },
+ });
+
+ await sendReportEmbed(report.id);
+ }
+ }
+
+ await prisma.log.create({
+ data: {
+ type,
+ browser: headersList.get("user-agent") || "unknown",
+ userId: user?.user.id || otherValues?.userId,
+ deviceId: deviceId,
+ ip,
+ ...otherValues,
+ },
+ });
+};
diff --git a/apps/hub/app/(auth)/passwort-reset/action.ts b/apps/hub/app/(auth)/passwort-reset/action.ts
index d4d7b8bb..425623ea 100644
--- a/apps/hub/app/(auth)/passwort-reset/action.ts
+++ b/apps/hub/app/(auth)/passwort-reset/action.ts
@@ -11,6 +11,7 @@ export const resetPassword = async (email: string) => {
let user = await prisma.user.findFirst({
where: {
email,
+ isDeleted: false,
},
});
const oldUser = (v1User as OldUser[]).find((u) => u.email.toLowerCase() === email);
diff --git a/apps/hub/app/(auth)/register/_components/Register.tsx b/apps/hub/app/(auth)/register/_components/Register.tsx
index 2990cdeb..0c67eb6e 100644
--- a/apps/hub/app/(auth)/register/_components/Register.tsx
+++ b/apps/hub/app/(auth)/register/_components/Register.tsx
@@ -9,6 +9,7 @@ import { useState } from "react";
import { Button } from "../../../_components/ui/Button";
import { sendVerificationLink } from "(app)/admin/user/action";
import toast from "react-hot-toast";
+import { logAction } from "(auth)/login/_components/action";
export const Register = () => {
const schema = z
@@ -93,6 +94,9 @@ export const Register = () => {
return;
}
await sendVerificationLink(user.id);
+ await logAction("REGISTER", {
+ userId: user.id,
+ });
await signIn("credentials", {
callbackUrl: "/",
email: user.email,
diff --git a/apps/hub/app/(auth)/register/action.ts b/apps/hub/app/(auth)/register/action.ts
index 5404372b..4a55dfdc 100644
--- a/apps/hub/app/(auth)/register/action.ts
+++ b/apps/hub/app/(auth)/register/action.ts
@@ -24,9 +24,6 @@ export const register = async ({ password, ...user }: Omit
deleteBooking(booking.id)}
- className={`btn btn-xs ${
- currentUser?.permissions.includes("ADMIN_EVENT") &&
- booking.User.publicId !== currentUser.publicId
- ? "btn-error"
- : "btn-neutral"
- }`}
+ className={`btn btn-xs btn-error`}
title="Buchung löschen"
>
diff --git a/apps/hub/app/_components/PaginatedTable.tsx b/apps/hub/app/_components/PaginatedTable.tsx
index af4aedc0..b2be55f8 100644
--- a/apps/hub/app/_components/PaginatedTable.tsx
+++ b/apps/hub/app/_components/PaginatedTable.tsx
@@ -21,6 +21,7 @@ interface PaginatedTableProps
leftOfSearch?: React.ReactNode;
rightOfSearch?: React.ReactNode;
leftOfPagination?: React.ReactNode;
+ rightOfPagination?: React.ReactNode;
supressQuery?: boolean;
ref?: Ref;
}
@@ -37,6 +38,7 @@ export function PaginatedTable({
leftOfSearch,
rightOfSearch,
leftOfPagination,
+ rightOfPagination,
supressQuery,
...restProps
}: PaginatedTableProps) {
@@ -159,10 +161,9 @@ export function PaginatedTable({
{leftOfPagination}
- <>
-
-
- >
+
+ {rightOfPagination}
+
);
diff --git a/apps/hub/app/_components/Table.tsx b/apps/hub/app/_components/Table.tsx
index e8ee6618..f2a51155 100644
--- a/apps/hub/app/_components/Table.tsx
+++ b/apps/hub/app/_components/Table.tsx
@@ -95,7 +95,7 @@ export const RowsPerPage = ({
}) => {
return (