completed user admin page

This commit is contained in:
PxlLoewe
2025-03-15 11:09:55 -07:00
parent abf3475c7c
commit 37d02ea0bc
23 changed files with 567 additions and 106 deletions

View File

@@ -14,7 +14,6 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const session = await getServerSession(); const session = await getServerSession();
console.log(session);
if (!session) { if (!session) {
redirect("/login"); redirect("/login");
} }

View File

@@ -1,8 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import { socket } from "../(dispatch)/socket"; import { socket } from "../(dispatch)/socket";
console.log("connectionStore");
interface ConnectionStore { interface ConnectionStore {
isConnected: boolean; isConnected: boolean;
connect: (uid: string) => Promise<void>; connect: (uid: string) => Promise<void>;

View File

@@ -1,3 +1,4 @@
API_PORT=
MOODLE_TOKEN= MOODLE_TOKEN=
MOODLE_URL= MOODLE_URL=
MAIL_SERVER= MAIL_SERVER=

View File

@@ -1,5 +1,17 @@
import "dotenv/config"; import "dotenv/config";
import "modules/chron"; import "modules/chron";
import express from "express";
import cors from "cors";
import router from "routes/router";
// Add API eventually const app = express();
console.log("VAR hub Server started");
app.use(express.json());
app.use(cors());
app.use(router);
const port = process.env.API_PORT || 3003;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

View File

@@ -0,0 +1,202 @@
import * as React from "react";
import { User } from "@repo/db";
import { Html, render } from "@react-email/components";
const styles = `
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: inherit !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
}
p {
line-height: inherit;
}
.desktop_hide,
.desktop_hide table {
mso-hide: all;
display: none;
max-height: 0px;
overflow: hidden;
}
.image_block img + div {
display: none;
}
sup,
sub {
font-size: 75%;
line-height: 0;
}
.menu_block.desktop_hide .menu-links span {
mso-hide: all;
}
@media (max-width: 700px) {
.desktop_hide table.icons-inner {
display: inline-block !important;
}
.icons-inner {
text-align: center;
}
.icons-inner td {
margin: 0 auto;
}
}
`;
const Template = ({ user, password }: { user: User; password: string }) => (
<Html lang="de">
<meta content="text/html; charset=utf-8" httpEquiv="Content-Type" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<link
href="https://fonts.googleapis.com/css?family=Montserrat"
rel="stylesheet"
type="text/css"
/>
<style>{styles}</style>
<body style={{ backgroundColor: "#FFFFFF", margin: 0, padding: 0 }}>
<table width="100%">
<tbody>
<tr>
<td>
<table
align="center"
width="680"
style={{ margin: "0 auto", color: "#000000" }}
>
<tbody>
<tr>
<td style={{ textAlign: "center", paddingTop: "30px" }}>
<img
src={`${process.env.HUB_URL}/mail/var_logo.png`}
alt="Logo"
width="80"
style={{ display: "block", margin: "0 auto" }}
/>
</td>
</tr>
<tr>
<td
style={{
textAlign: "center",
fontSize: "46px",
color: "#011936",
}}
>
<strong>Passwort geändert</strong>
</td>
</tr>
<tr>
<td
style={{
textAlign: "center",
fontSize: "30px",
color: "#011936",
}}
>
Hallo {user.firstname},
</td>
</tr>
<tr>
<td
style={{
textAlign: "center",
fontSize: "18px",
color: "#011936",
padding: "20px",
}}
>
Dein Passwort wurde erfolgreich geändert. Wenn du diese
Änderung nicht vorgenommen hast, kontaktiere bitte sofort
unseren Support.
</td>
</tr>
<tr>
<td
style={{
textAlign: "center",
fontSize: "18px",
color: "#011936",
padding: "20px",
}}
>
Dein neues Passwort lautet: <strong>{password}</strong>
</td>
</tr>
<tr>
<td style={{ textAlign: "center", paddingTop: "20px" }}>
<a
href="https://your-platform.com"
style={{
padding: "10px",
textDecoration: "none",
borderRadius: "20px",
color: "#011936",
}}
>
Impressum
</a>
<span style={{ margin: "0 10px" }}>|</span>
<a
href="https://your-platform.com"
style={{
padding: "10px",
textDecoration: "none",
borderRadius: "20px",
color: "#011936",
}}
>
Datenschutzerklärung
</a>
<span style={{ margin: "0 10px" }}>|</span>
<a
href="https://your-platform.com"
style={{
padding: "10px",
textDecoration: "none",
borderRadius: "20px",
color: "#011936",
}}
>
Knowledgebase
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</Html>
);
export function renderPasswordChanged({
user,
password,
}: {
user: User;
password: string;
}) {
return render(<Template user={user} password={password} />);
}

View File

@@ -1,6 +1,7 @@
import { Event, User } from "@repo/db"; import { Event, User } from "@repo/db";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import { renderCourseCompleted } from "./mail-templates/CourseCompleted"; import { renderCourseCompleted } from "./mail-templates/CourseCompleted";
import { renderPasswordChanged } from "./mail-templates/PasswordChanged";
let transporter: nodemailer.Transporter | null = null; let transporter: nodemailer.Transporter | null = null;
@@ -42,18 +43,39 @@ export const sendCourseCompletedEmail = async (
console.error("Transporter is not initialized"); console.error("Transporter is not initialized");
return; return;
} }
transporter.sendMail( sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
{
from: process.env.MAIL_USER,
to,
subject: `Kurs ${event.name} erfolgreich abgeschlossen`,
html: emailHtml,
},
(error, info) => {
if (error) {
console.error("Error sending email:", error);
} else {
}
},
);
}; };
export const sendPasswordChanged = async (
to: string,
user: User,
password: string,
) => {
const emailHtml = await renderPasswordChanged({ user, password });
await sendMail(to, `Dein Passwort wurde geändert`, emailHtml);
};
export const sendMail = async (to: string, subject: string, html: string) =>
new Promise<void>(async (resolve, reject) => {
if (!transporter) {
console.error("Transporter is not initialized");
return;
}
await transporter.sendMail(
{
from: process.env.MAIL_USER,
to,
subject: subject,
html,
},
(error, info) => {
if (error) {
reject(error);
console.error("Error sending email:", error);
} else {
resolve();
}
},
);
});

View File

@@ -18,8 +18,10 @@
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.33", "@react-email/components": "^0.0.33",
"axios": "^1.7.9", "axios": "^1.7.9",
"cors": "^2.8.5",
"cron": "^4.1.0", "cron": "^4.1.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"react": "^19.0.0" "react": "^19.0.0"
} }

View File

@@ -0,0 +1,56 @@
import { Router } from "express";
import { sendMail } from "modules/mail";
import { sendPasswordChanged, sendCourseCompletedEmail } from "modules/mail";
const router = Router();
router.post("/send", async (req, res) => {
console.log(req.body);
const { to, subject, html } = req.body;
try {
await sendMail(to, subject, html);
// Send email logic here
res.status(200).json({ message: "Email sent successfully" });
} catch (error) {
res.status(500).json({ error: "Failed to send email" });
}
});
router.post("/template/:template", async (req, res) => {
const { template } = req.params;
const { to, data } = req.body;
if (!to || !data) {
res.status(400).json({ error: "Missing required fields" });
return;
}
if (!data.user) {
res.status(400).json({ error: "Missing user data" });
return;
}
console.log("template", template);
switch (template) {
case "password-change":
if (!data.password) {
res.status(400).json({ error: "Missing password data" });
return;
}
await sendPasswordChanged(to, data.user, data.password);
break;
case "course-completed":
if (!data.event) {
res.status(400).json({ error: "Missing event data" });
return;
}
await sendCourseCompletedEmail(to, data.user, data.event);
break;
default:
res.status(400).json({ error: "Invalid template" });
return;
}
res.status(200).json({ message: "Email sent successfully" });
});
export default router;

View File

@@ -0,0 +1,8 @@
import { Router } from "express";
import mailRouter from "./mail";
const router = Router();
router.use("/mail", mailRouter);
export default router;

View File

@@ -62,7 +62,6 @@ export default async () => {
}); });
const filteredEvents = events.filter((event) => { const filteredEvents = events.filter((event) => {
console.log;
if (eventCompleted(event, event.participants[0])) return false; if (eventCompleted(event, event.participants[0])) return false;
if ( if (
event.type === "OBLIGATED_COURSE" && event.type === "OBLIGATED_COURSE" &&

View File

@@ -61,9 +61,8 @@ export const AppointmentModal = ({
</h3> </h3>
<form <form
onSubmit={appointmentForm.handleSubmit(async (values) => { onSubmit={appointmentForm.handleSubmit(async (values) => {
console.log(values);
if (!event) return; if (!event) return;
const createdAppointment = await upsertAppointment(values); await upsertAppointment(values);
ref.current?.close(); ref.current?.close();
appointmentsTableRef.current?.refresh(); appointmentsTableRef.current?.refresh();
})} })}

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { BADGES, User } from "@repo/db"; import { BADGES, PERMISSION, User } from "@repo/db";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { updateUser } from "../../../../settings/actions"; import { editUser, resetPassword } from "../../action";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { import {
PersonIcon, PersonIcon,
@@ -18,36 +18,25 @@ import {
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { Button } from "../../../../../_components/ui/Button"; import { Button } from "../../../../../_components/ui/Button";
import { Select } from "../../../../../_components/ui/Select"; import { Select } from "../../../../../_components/ui/Select";
import { UserSchema } from "@repo/db/zod";
import { useRouter } from "next/navigation";
interface ProfileFormProps { interface ProfileFormProps {
user: User | null; user: User;
} }
export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => { export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
const schema = z.object({
firstname: z.string().min(2).max(30),
lastname: z.string().min(2).max(30),
email: z.string().email({
message: "Bitte gebe eine gültige E-Mail Adresse ein",
}),
});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
type IFormInput = z.infer<typeof schema>; const form = useForm<User>({
defaultValues: user,
const form = useForm<IFormInput>({ resolver: zodResolver(UserSchema),
defaultValues: {
firstname: user?.firstname,
lastname: user?.lastname,
email: user?.email,
},
resolver: zodResolver(schema),
}); });
return ( return (
<form <form
className="card-body" className="card-body"
onSubmit={form.handleSubmit(async (values) => { onSubmit={form.handleSubmit(async (values) => {
setIsLoading(true); setIsLoading(true);
await updateUser(values); await editUser(values.id, values);
form.reset(values); form.reset(values);
setIsLoading(false); setIsLoading(false);
toast.success("Deine Änderungen wurden gespeichert!", { toast.success("Deine Änderungen wurden gespeichert!", {
@@ -114,13 +103,23 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
<Select <Select
isMulti isMulti
form={form} form={form}
name="finishedBadges" name="badges"
label="Badges" label="Badges"
options={Object.entries(BADGES).map(([key, value]) => ({ options={Object.entries(BADGES).map(([key, value]) => ({
label: value, label: value,
value: key, value: key,
}))} }))}
/> />
<Select
isMulti
form={form}
name="permissions"
label="Permissions"
options={Object.entries(PERMISSION).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
<div className="card-actions justify-center pt-6"> <div className="card-actions justify-center pt-6">
<Button <Button
role="submit" role="submit"
@@ -137,6 +136,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
}; };
export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => { export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
const router = useRouter();
return ( return (
<div className="card-body"> <div className="card-body">
<h2 className="card-title"> <h2 className="card-title">
@@ -145,23 +146,59 @@ export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
<div className="text-left"> <div className="text-left">
<div className="card-actions pt-6"> <div className="card-actions pt-6">
<Button <Button
role="submit" onClick={async () => {
const { password } = await resetPassword(user.id);
toast.success(
`Neues Passwort
${password}, es wurde dem Nutzer an die E-Mail gesendet!`,
{
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
},
);
}}
className="btn-sm btn-wide btn-outline btn-success" className="btn-sm btn-wide btn-outline btn-success"
> >
<LockOpen1Icon /> Passwort zurücksetzen <LockOpen1Icon /> Passwort zurücksetzen
</Button> </Button>
<Button {!user.isBanned && (
role="submit" <Button
className="btn-sm btn-wide btn-outline btn-error" onClick={async () => {
> await editUser(user.id, { isBanned: true });
<HobbyKnifeIcon /> User Sperren toast.success("Nutzer wurde gesperrt!", {
</Button> style: {
<Button background: "var(--color-base-100)",
role="submit" color: "var(--color-base-content)",
className="btn-sm btn-wide btn-outline btn-warning" },
> });
<HeartIcon /> User Entperren router.refresh();
</Button> }}
role="submit"
className="btn-sm btn-wide btn-outline btn-error"
>
<HobbyKnifeIcon /> User Sperren
</Button>
)}
{user.isBanned && (
<Button
onClick={async () => {
await editUser(user.id, { isBanned: false });
toast.success("Nutzer wurde entsperrt!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
router.refresh();
}}
role="submit"
className="btn-sm btn-wide btn-outline btn-warning"
>
<HobbyKnifeIcon /> User Entperren
</Button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,19 +1,18 @@
import { PersonIcon } from "@radix-ui/react-icons"; import { PersonIcon } from "@radix-ui/react-icons";
import { PrismaClient, User } from "@repo/db"; import { PrismaClient, User } from "@repo/db";
import { AdminForm, ProfileForm } from "./_components/forms"; import { AdminForm, ProfileForm } from "./_components/forms";
import { Error } from "../../../../_components/Error";
export default async ({ params }: { params: { id: string } }) => { export default async ({ params }: { params: { id: string } }) => {
const prisma = new PrismaClient(); const prisma = new PrismaClient();
const { id } = params; const { id } = await params;
const user: User | null = await prisma.user.findUnique({ const user: User | null = await prisma.user.findUnique({
where: { where: {
id: id, id: id,
}, },
}); });
if (!user) return <Error statusCode={404} title="User not found" />;
console.log(user);
return ( return (
<div className="grid grid-cols-6 gap-4"> <div className="grid grid-cols-6 gap-4">
<div className="col-span-full"> <div className="col-span-full">

View File

@@ -0,0 +1,35 @@
"use server";
import { PrismaClient } from "@prisma/client";
import { prisma, Prisma } from "@repo/db";
import bcrypt from "bcryptjs";
import { sendMailByTemplate } from "../../../../helper/mail";
export const editUser = async (id: string, data: Prisma.UserUpdateInput) => {
return await prisma.user.update({
where: {
id: id,
},
data,
});
};
export const resetPassword = async (id: string) => {
const password = Math.random().toString(36).slice(-8);
const hashedPassword = await bcrypt.hash(password, 15);
const user = await prisma.user.update({
where: {
id: id,
},
data: {
password: hashedPassword,
},
});
await sendMailByTemplate(user.email, "password-change", {
user: user,
password: password,
});
return { password };
};

View File

@@ -103,13 +103,6 @@ const ModalBtn = ({
? (selectedDate as any)?.Participants?.length + 1 ? (selectedDate as any)?.Participants?.length + 1
: ownIndexInParticipantList + 1; : ownIndexInParticipantList + 1;
console.log({
selectedDate,
ownPlaceInParticipantList,
ownIndexInParticipantList,
maxParticipants: event.maxParticipants,
});
return ( return (
<> <>
<button <button

View File

@@ -43,7 +43,7 @@ export const Register = () => {
passwordConfirm: "", passwordConfirm: "",
}, },
}); });
console.log(form.formState.errors);
return ( return (
<form <form
className="card-body" className="card-body"

View File

@@ -0,0 +1,11 @@
"use client";
export const Error = ({
statusCode,
title,
}: {
statusCode: number;
title: string;
}) => {
return <Error statusCode={404} title="User not found" />;
};

View File

@@ -1,25 +1,43 @@
import { ButtonHTMLAttributes, DetailedHTMLProps } from 'react'; import {
import { cn } from '../../../helper/cn'; ButtonHTMLAttributes,
DetailedHTMLProps,
useEffect,
useState,
} from "react";
import { cn } from "../../../helper/cn";
export const Button = ({ export const Button = ({
isLoading, isLoading,
...props ...props
}: DetailedHTMLProps< }: DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>, ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement HTMLButtonElement
> & { > & {
isLoading?: boolean; isLoading?: boolean;
}) => { }) => {
return ( const [isLoadingState, setIsLoadingState] = useState(isLoading);
<button
{...(props as any)} useEffect(() => {
className={cn('btn', props.className)} setIsLoadingState(isLoading);
disabled={isLoading || props.disabled} }, [isLoading]);
>
{isLoading && ( return (
<span className="loading loading-spinner loading-sm"></span> <button
)} {...(props as any)}
{props.children as any} className={cn("btn", props.className)}
</button> disabled={isLoadingState || props.disabled}
); onClick={async (e) => {
if (props.onClick) {
setIsLoadingState(true);
await props.onClick(e);
setIsLoadingState(false);
}
}}
>
{isLoadingState && (
<span className="loading loading-spinner loading-sm"></span>
)}
{props.children as any}
</button>
);
}; };

View File

@@ -76,11 +76,16 @@ const SelectCom = <T extends FieldValues>({
<SelectTemplate <SelectTemplate
onChange={(newValue: any) => { onChange={(newValue: any) => {
if (Array.isArray(newValue)) { if (Array.isArray(newValue)) {
form.setValue(name, newValue.map((v: any) => v.value) as any); form.setValue(name, newValue.map((v: any) => v.value) as any, {
shouldDirty: true,
});
} else { } else {
form.setValue(name, newValue.value); form.setValue(name, newValue.value, {
shouldDirty: true,
});
} }
form.trigger(name); form.trigger(name);
form.Dirty;
}} }}
value={ value={
(inputProps as any)?.isMulti (inputProps as any)?.isMulti

View File

@@ -24,9 +24,7 @@ export const GET = async (req: NextRequest) => {
if (!user) if (!user)
return NextResponse.json({ error: "User not found" }, { status: 404 }); return NextResponse.json({ error: "User not found" }, { status: 404 });
setTimeout(async () => { setTimeout(async () => {
console.log("getting moodle ID");
const moodleUser = await getMoodleUserById(user.id); const moodleUser = await getMoodleUserById(user.id);
console.log("got moodle ID", moodleUser.id);
await prisma.user.update({ await prisma.user.update({
where: { where: {
id: user.id, id: user.id,

40
apps/hub/helper/mail.ts Normal file
View File

@@ -0,0 +1,40 @@
export const sendMail = async (
email: string,
subject: string,
html: string,
) => {
await fetch(`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/mail/send`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to: email,
subject,
html,
}),
});
};
export const sendMailByTemplate = async (
email: string,
template: "password-change" | "course-completed",
data: any,
) => {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/mail/template/${template}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to: email,
data,
}),
},
);
console.log(res);
} catch (error) {
console.error("Error sending mail:", error);
}
};

27
package-lock.json generated
View File

@@ -140,8 +140,10 @@
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.33", "@react-email/components": "^0.0.33",
"axios": "^1.7.9", "axios": "^1.7.9",
"cors": "^2.8.5",
"cron": "^4.1.0", "cron": "^4.1.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"react": "^19.0.0" "react": "^19.0.0"
}, },
@@ -3798,6 +3800,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"clean-stack": "^2.0.0", "clean-stack": "^2.0.0",
@@ -3893,6 +3896,7 @@
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-convert": "^1.9.0" "color-convert": "^1.9.0"
@@ -4236,6 +4240,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": { "node_modules/base64-js": {
@@ -4358,6 +4363,7 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -4569,6 +4575,7 @@
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^3.2.1", "ansi-styles": "^3.2.1",
@@ -4667,6 +4674,7 @@
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -4835,6 +4843,7 @@
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "1.1.3" "color-name": "1.1.3"
@@ -4844,6 +4853,7 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": { "node_modules/color-string": {
@@ -4919,6 +4929,7 @@
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/concurrently": { "node_modules/concurrently": {
@@ -6061,6 +6072,7 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
@@ -7096,6 +7108,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
@@ -7287,6 +7300,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported", "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
@@ -7536,6 +7550,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@@ -8091,6 +8106,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -8101,6 +8117,7 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"once": "^1.3.0", "once": "^1.3.0",
@@ -8117,6 +8134,7 @@
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/inline-style-parser": { "node_modules/inline-style-parser": {
@@ -8866,6 +8884,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/iterator.prototype": { "node_modules/iterator.prototype": {
@@ -10560,6 +10579,7 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@@ -13582,6 +13602,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
@@ -13814,6 +13835,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
"integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"aggregate-error": "^3.0.0" "aggregate-error": "^3.0.0"
@@ -13996,6 +14018,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -16281,6 +16304,7 @@
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
@@ -17178,6 +17202,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
"integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
@@ -17353,6 +17378,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
@@ -17525,6 +17551,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {

View File

@@ -17,21 +17,21 @@ enum PERMISSION {
} }
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
publicId String @unique publicId String @unique
firstname String firstname String
lastname String lastname String
email String @unique email String @unique
password String password String
vatsimCid Int? @map(name: "vatsim_cid") vatsimCid Int? @map(name: "vatsim_cid")
moodleId Int? @map(name: "moodle_id") moodleId Int? @map(name: "moodle_id")
emailVerified DateTime? @map(name: "email_verified") emailVerified DateTime? @map(name: "email_verified")
image String? image String?
badges BADGES[] @default([]) badges BADGES[] @default([])
permissions PERMISSION[] @default([]) permissions PERMISSION[] @default([])
createdAt DateTime @default(now()) @map(name: "created_at") createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at") updatedAt DateTime @default(now()) @map(name: "updated_at")
isBanned Boolean @default(false) @map(name: "is_banned")
// relations: // relations:
oauthTokens OAuthToken[] oauthTokens OAuthToken[]
discordAccounts DiscordAccount[] discordAccounts DiscordAccount[]