completed user admin page
This commit is contained in:
@@ -14,7 +14,6 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getServerSession();
|
||||
console.log(session);
|
||||
if (!session) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { socket } from "../(dispatch)/socket";
|
||||
|
||||
console.log("connectionStore");
|
||||
|
||||
interface ConnectionStore {
|
||||
isConnected: boolean;
|
||||
connect: (uid: string) => Promise<void>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
API_PORT=
|
||||
MOODLE_TOKEN=
|
||||
MOODLE_URL=
|
||||
MAIL_SERVER=
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import "dotenv/config";
|
||||
import "modules/chron";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import router from "routes/router";
|
||||
|
||||
// Add API eventually
|
||||
console.log("VAR hub Server started");
|
||||
const app = express();
|
||||
|
||||
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}`);
|
||||
});
|
||||
|
||||
202
apps/hub-server/modules/mail-templates/PasswordChanged.tsx
Normal file
202
apps/hub-server/modules/mail-templates/PasswordChanged.tsx
Normal 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} />);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Event, User } from "@repo/db";
|
||||
import nodemailer from "nodemailer";
|
||||
import { renderCourseCompleted } from "./mail-templates/CourseCompleted";
|
||||
import { renderPasswordChanged } from "./mail-templates/PasswordChanged";
|
||||
|
||||
let transporter: nodemailer.Transporter | null = null;
|
||||
|
||||
@@ -42,18 +43,39 @@ export const sendCourseCompletedEmail = async (
|
||||
console.error("Transporter is not initialized");
|
||||
return;
|
||||
}
|
||||
transporter.sendMail(
|
||||
sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
|
||||
};
|
||||
|
||||
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: `Kurs ${event.name} erfolgreich abgeschlossen`,
|
||||
html: emailHtml,
|
||||
subject: subject,
|
||||
html,
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
console.error("Error sending email:", error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
"dependencies": {
|
||||
"@react-email/components": "^0.0.33",
|
||||
"axios": "^1.7.9",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
|
||||
56
apps/hub-server/routes/mail.ts
Normal file
56
apps/hub-server/routes/mail.ts
Normal 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;
|
||||
8
apps/hub-server/routes/router.ts
Normal file
8
apps/hub-server/routes/router.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import mailRouter from "./mail";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use("/mail", mailRouter);
|
||||
|
||||
export default router;
|
||||
@@ -62,7 +62,6 @@ export default async () => {
|
||||
});
|
||||
|
||||
const filteredEvents = events.filter((event) => {
|
||||
console.log;
|
||||
if (eventCompleted(event, event.participants[0])) return false;
|
||||
if (
|
||||
event.type === "OBLIGATED_COURSE" &&
|
||||
|
||||
@@ -61,9 +61,8 @@ export const AppointmentModal = ({
|
||||
</h3>
|
||||
<form
|
||||
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
||||
console.log(values);
|
||||
if (!event) return;
|
||||
const createdAppointment = await upsertAppointment(values);
|
||||
await upsertAppointment(values);
|
||||
ref.current?.close();
|
||||
appointmentsTableRef.current?.refresh();
|
||||
})}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { BADGES, User } from "@repo/db";
|
||||
import { BADGES, PERMISSION, User } from "@repo/db";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { updateUser } from "../../../../settings/actions";
|
||||
import { editUser, resetPassword } from "../../action";
|
||||
import { toast } from "react-hot-toast";
|
||||
import {
|
||||
PersonIcon,
|
||||
@@ -18,36 +18,25 @@ import {
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Button } from "../../../../../_components/ui/Button";
|
||||
import { Select } from "../../../../../_components/ui/Select";
|
||||
import { UserSchema } from "@repo/db/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface ProfileFormProps {
|
||||
user: User | null;
|
||||
user: 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);
|
||||
type IFormInput = z.infer<typeof schema>;
|
||||
|
||||
const form = useForm<IFormInput>({
|
||||
defaultValues: {
|
||||
firstname: user?.firstname,
|
||||
lastname: user?.lastname,
|
||||
email: user?.email,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
const form = useForm<User>({
|
||||
defaultValues: user,
|
||||
resolver: zodResolver(UserSchema),
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="card-body"
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
setIsLoading(true);
|
||||
await updateUser(values);
|
||||
await editUser(values.id, values);
|
||||
form.reset(values);
|
||||
setIsLoading(false);
|
||||
toast.success("Deine Änderungen wurden gespeichert!", {
|
||||
@@ -114,13 +103,23 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
<Select
|
||||
isMulti
|
||||
form={form}
|
||||
name="finishedBadges"
|
||||
name="badges"
|
||||
label="Badges"
|
||||
options={Object.entries(BADGES).map(([key, value]) => ({
|
||||
label: value,
|
||||
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">
|
||||
<Button
|
||||
role="submit"
|
||||
@@ -137,6 +136,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
};
|
||||
|
||||
export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
@@ -145,23 +146,59 @@ export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
|
||||
<div className="text-left">
|
||||
<div className="card-actions pt-6">
|
||||
<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"
|
||||
>
|
||||
<LockOpen1Icon /> Passwort zurücksetzen
|
||||
</Button>
|
||||
{!user.isBanned && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await editUser(user.id, { isBanned: true });
|
||||
toast.success("Nutzer wurde gesperrt!", {
|
||||
style: {
|
||||
background: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
},
|
||||
});
|
||||
router.refresh();
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<HeartIcon /> User Entperren
|
||||
<HobbyKnifeIcon /> User Entperren
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { PersonIcon } from "@radix-ui/react-icons";
|
||||
import { PrismaClient, User } from "@repo/db";
|
||||
import { AdminForm, ProfileForm } from "./_components/forms";
|
||||
import { Error } from "../../../../_components/Error";
|
||||
|
||||
export default async ({ params }: { params: { id: string } }) => {
|
||||
const prisma = new PrismaClient();
|
||||
const { id } = params;
|
||||
const { id } = await params;
|
||||
|
||||
const user: User | null = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(user);
|
||||
|
||||
if (!user) return <Error statusCode={404} title="User not found" />;
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-full">
|
||||
|
||||
35
apps/hub/app/(app)/admin/user/action.ts
Normal file
35
apps/hub/app/(app)/admin/user/action.ts
Normal 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 };
|
||||
};
|
||||
@@ -103,13 +103,6 @@ const ModalBtn = ({
|
||||
? (selectedDate as any)?.Participants?.length + 1
|
||||
: ownIndexInParticipantList + 1;
|
||||
|
||||
console.log({
|
||||
selectedDate,
|
||||
ownPlaceInParticipantList,
|
||||
ownIndexInParticipantList,
|
||||
maxParticipants: event.maxParticipants,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Register = () => {
|
||||
passwordConfirm: "",
|
||||
},
|
||||
});
|
||||
console.log(form.formState.errors);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="card-body"
|
||||
|
||||
11
apps/hub/app/_components/Error.tsx
Normal file
11
apps/hub/app/_components/Error.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const Error = ({
|
||||
statusCode,
|
||||
title,
|
||||
}: {
|
||||
statusCode: number;
|
||||
title: string;
|
||||
}) => {
|
||||
return <Error statusCode={404} title="User not found" />;
|
||||
};
|
||||
@@ -1,5 +1,10 @@
|
||||
import { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
|
||||
import { cn } from '../../../helper/cn';
|
||||
import {
|
||||
ButtonHTMLAttributes,
|
||||
DetailedHTMLProps,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { cn } from "../../../helper/cn";
|
||||
|
||||
export const Button = ({
|
||||
isLoading,
|
||||
@@ -10,13 +15,26 @@ export const Button = ({
|
||||
> & {
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
const [isLoadingState, setIsLoadingState] = useState(isLoading);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingState(isLoading);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...(props as any)}
|
||||
className={cn('btn', props.className)}
|
||||
disabled={isLoading || props.disabled}
|
||||
className={cn("btn", props.className)}
|
||||
disabled={isLoadingState || props.disabled}
|
||||
onClick={async (e) => {
|
||||
if (props.onClick) {
|
||||
setIsLoadingState(true);
|
||||
await props.onClick(e);
|
||||
setIsLoadingState(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isLoading && (
|
||||
{isLoadingState && (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
{props.children as any}
|
||||
|
||||
@@ -76,11 +76,16 @@ const SelectCom = <T extends FieldValues>({
|
||||
<SelectTemplate
|
||||
onChange={(newValue: any) => {
|
||||
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 {
|
||||
form.setValue(name, newValue.value);
|
||||
form.setValue(name, newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
form.trigger(name);
|
||||
form.Dirty;
|
||||
}}
|
||||
value={
|
||||
(inputProps as any)?.isMulti
|
||||
|
||||
@@ -24,9 +24,7 @@ export const GET = async (req: NextRequest) => {
|
||||
if (!user)
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
setTimeout(async () => {
|
||||
console.log("getting moodle ID");
|
||||
const moodleUser = await getMoodleUserById(user.id);
|
||||
console.log("got moodle ID", moodleUser.id);
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
|
||||
40
apps/hub/helper/mail.ts
Normal file
40
apps/hub/helper/mail.ts
Normal 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
27
package-lock.json
generated
@@ -140,8 +140,10 @@
|
||||
"dependencies": {
|
||||
"@react-email/components": "^0.0.33",
|
||||
"axios": "^1.7.9",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
@@ -3798,6 +3800,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clean-stack": "^2.0.0",
|
||||
@@ -3893,6 +3896,7 @@
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
@@ -4236,6 +4240,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
@@ -4358,6 +4363,7 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -4569,6 +4575,7 @@
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
@@ -4667,6 +4674,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -4835,6 +4843,7 @@
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
@@ -4844,6 +4853,7 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
@@ -4919,6 +4929,7 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
@@ -6061,6 +6072,7 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
@@ -7096,6 +7108,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
@@ -7287,6 +7300,7 @@
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
@@ -7536,6 +7550,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -8091,6 +8106,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -8101,6 +8117,7 @@
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"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.",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
@@ -8117,6 +8134,7 @@
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
@@ -8866,6 +8884,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
@@ -10560,6 +10579,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -13582,6 +13602,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
@@ -13814,6 +13835,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
|
||||
"integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
@@ -13996,6 +14018,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -16281,6 +16304,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
@@ -17178,6 +17202,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
|
||||
"integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
@@ -17353,6 +17378,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -17525,6 +17551,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
|
||||
@@ -31,7 +31,7 @@ model User {
|
||||
permissions PERMISSION[] @default([])
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
isBanned Boolean @default(false) @map(name: "is_banned")
|
||||
// relations:
|
||||
oauthTokens OAuthToken[]
|
||||
discordAccounts DiscordAccount[]
|
||||
|
||||
Reference in New Issue
Block a user