diff --git a/apps/hub-server/modules/mail-templates/Bann.tsx b/apps/hub-server/modules/mail-templates/Bann.tsx new file mode 100644 index 00000000..5f547bbf --- /dev/null +++ b/apps/hub-server/modules/mail-templates/Bann.tsx @@ -0,0 +1,166 @@ +import * as React from "react"; +import { User } from "@repo/db"; +import { Html, render } from "@react-email/components"; +import { EmailFooter } from "./EmailFooter"; + +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 PenaltyNoticeTemplate = ({ user, staffName }: { user: User; staffName: string }) => ( + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ Logo +
+ Deine dauerhafte Sperrung bei Virtual Air Rescue +
+ Hallo {user.firstname}, +
+ wir möchten dich darüber informieren, dass dein Account dauerhaft von unserer + Plattform Virtual Air Rescue ausgeschlossen wurde. +
+ Grund hierfür ist ein Verstoß gegen unsere Richtlinien. Eine Rückkehr auf die + Plattform ist leider nicht mehr möglich. +
+ Für Fragen oder Einsicht in die Entscheidung kannst du dich direkt an uns + wenden. +
+ Mit freundlichen Grüßen +
+ {staffName} +
+ Virtual Air Rescue +
+
+ + +); + +export function renderBannNotice({ user, staffName }: { user: User; staffName: string }) { + return render(); +} diff --git a/apps/hub-server/modules/mail-templates/TimeBann.tsx b/apps/hub-server/modules/mail-templates/TimeBann.tsx new file mode 100644 index 00000000..a12eb60e --- /dev/null +++ b/apps/hub-server/modules/mail-templates/TimeBann.tsx @@ -0,0 +1,168 @@ +import * as React from "react"; +import { User } from "@repo/db"; +import { Html, render } from "@react-email/components"; +import { EmailFooter } from "./EmailFooter"; + +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 TimeBanTemplate = ({ user, staffName }: { user: User; staffName: string }) => ( + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ Logo +
+ + Zeitlich begrenzte Sperrung deines Accounts bei Virtual Air Rescue + +
+ Hallo {user.firstname}, +
+ Aufgrund eines Regelverstoßes wurde dein Account bei Virtual Air Rescue + zeitlich begrenzt gesperrt. +
+ Die Sperre bleibt bis zum Ablauf der definierten Frist aktiv. Danach kannst du + die Plattform wie gewohnt weiter nutzen. +
+ Wir empfehlen dir, unsere Verhaltensrichtlinien noch einmal durchzulesen, um + zukünftige Sperren zu vermeiden. +
+ Mit freundlichen Grüßen +
+ {staffName} +
+ Virtual Air Rescue +
+
+ + +); + +export function renderTimeBanNotice({ user, staffName }: { user: User; staffName: string }) { + return render(); +} diff --git a/apps/hub-server/modules/mail.ts b/apps/hub-server/modules/mail.ts index 1eecc6b2..93f5fec6 100644 --- a/apps/hub-server/modules/mail.ts +++ b/apps/hub-server/modules/mail.ts @@ -3,6 +3,8 @@ import nodemailer from "nodemailer"; import { renderCourseCompleted } from "./mail-templates/CourseCompleted"; import { renderPasswordChanged } from "./mail-templates/PasswordChanged"; import { renderVerificationCode } from "./mail-templates/ConfirmEmail"; +import { renderBannNotice } from "modules/mail-templates/Bann"; +import { renderTimeBanNotice } from "modules/mail-templates/TimeBann"; let transporter: nodemailer.Transporter | null = null; @@ -55,6 +57,22 @@ export const sendEmailVerification = async (to: string, user: User, code: string await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml); }; +export const sendBannEmail = async (to: string, user: User, staffName: string) => { + const emailHtml = await renderBannNotice({ + user, + staffName, + }); + await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml); +}; + +export const sendTimebannEmail = async (to: string, user: User, staffName: string) => { + const emailHtml = await renderTimeBanNotice({ + user, + staffName, + }); + await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml); +}; + export const sendMail = async (to: string, subject: string, html: string) => new Promise(async (resolve, reject) => { if (!transporter) { diff --git a/apps/hub-server/routes/mail.ts b/apps/hub-server/routes/mail.ts index 522e35c3..98c778f3 100644 --- a/apps/hub-server/routes/mail.ts +++ b/apps/hub-server/routes/mail.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { sendEmailVerification, sendMail } from "modules/mail"; +import { sendBannEmail, sendEmailVerification, sendMail, sendTimebannEmail } from "modules/mail"; import { sendPasswordChanged, sendCourseCompletedEmail } from "modules/mail"; const router: Router = Router(); @@ -52,6 +52,21 @@ router.post("/template/:template", async (req, res) => { return; } await sendEmailVerification(to, data.user, data.code); + case "ban-notice": + if (!data.user || !data.staffName) { + res.status(400).json({ error: "Missing ban data" }); + return; + } + // Implement ban notice email logic here + await sendBannEmail(to, data.user, data.staffName); + break; + case "timeban-notice": + if (!data.user || !data.staffName) { + res.status(400).json({ error: "Missing timeban data" }); + return; + } + await sendTimebannEmail(to, data.user, data.staffName); + break; default: res.status(400).json({ error: "Invalid template" }); return; diff --git a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx index 029a087a..eeb2e7f2 100644 --- a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx @@ -56,6 +56,7 @@ import { setStandardName } from "../../../../../../helper/discord"; import { penaltyColumns } from "(app)/admin/penalty/columns"; import { addPenalty, editPenaltys } from "(app)/admin/penalty/actions"; import { reportColumns } from "(app)/admin/report/columns"; +import { sendMail, sendMailByTemplate } from "../../../../../../helper/mail"; interface ProfileFormProps { user: User; @@ -349,6 +350,12 @@ export const UserPenalties = ({ user }: { user: User }) => { userId: user.id, createdUserId: createdUser.id, }); + if (user.email) { + await sendMailByTemplate(user.email, "timeban-notice", { + user, + staffName: createdUser.firstname + " " + createdUser.lastname, + }); + } penaltyTable.current?.refresh(); toast.success("Time-Ban wurde hinzugefügt!"); }} @@ -370,6 +377,12 @@ export const UserPenalties = ({ user }: { user: User }) => { userId: user.id, createdUserId: createdUser.id, }); + if (user.email) { + await sendMailByTemplate(user.email, "ban-notice", { + user, + staffName: createdUser.firstname + " " + createdUser.lastname, + }); + } await editUser(user.id, { isBanned: true }); penaltyTable.current?.refresh(); toast.success("Ban wurde hinzugefügt!"); diff --git a/apps/hub/app/(app)/admin/user/action.ts b/apps/hub/app/(app)/admin/user/action.ts index 21895aaf..3b4db473 100644 --- a/apps/hub/app/(app)/admin/user/action.ts +++ b/apps/hub/app/(app)/admin/user/action.ts @@ -58,42 +58,6 @@ export const deleteUser = async (id: string) => { }); }; -export const checkEmailCode = async (code: string) => { - const users = await prisma.user.findMany({ - where: { - emailVerificationToken: code, - }, - select: { - id: true, - emailVerificationToken: true, - emailVerificationExpiresAt: true, - }, - }); - const user = users[0]; - - if (!user || !user.emailVerificationExpiresAt) { - return { error: "Code ist ungültig" }; - } - - if (user.emailVerificationExpiresAt < new Date()) { - return { error: "Code ist nicht mehr gültig" }; - } - - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - emailVerified: true, - emailVerificationToken: null, - emailVerificationExpiresAt: null, - }, - }); - return { - message: "Email bestätigt!", - }; -}; - export const sendVerificationLink = async (userId: string) => { const code = Math.floor(10000 + Math.random() * 90000).toString(); diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx index 6593b4e7..bdeb5e08 100644 --- a/apps/hub/app/(app)/settings/_components/forms.tsx +++ b/apps/hub/app/(app)/settings/_components/forms.tsx @@ -24,13 +24,16 @@ import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod"; import { Bell, CircleAlert, Plane, Trash2 } from "lucide-react"; import Link from "next/link"; import { deleteUser, sendVerificationLink } from "(app)/admin/user/action"; +import { setStandardName } from "../../../../helper/discord"; export const ProfileForm = ({ user, penaltys, + discordAccount, }: { user: User; penaltys: Penalty[]; + discordAccount?: DiscordAccount; }): React.JSX.Element => { const canEdit = penaltys.length === 0 && !user.isBanned; @@ -67,12 +70,19 @@ export const ProfileForm = ({ }, resolver: zodResolver(schema), }); + console.log(user); return (
{ setIsLoading(true); await updateUser(values); + if (discordAccount) { + await setStandardName({ + memberId: discordAccount.discordId, + userId: user.id, + }); + } form.reset(values); if (user.email !== values.email) { await sendVerificationLink(user.id); diff --git a/apps/hub/app/(app)/settings/page.tsx b/apps/hub/app/(app)/settings/page.tsx index dd3c1dea..ba0139f2 100644 --- a/apps/hub/app/(app)/settings/page.tsx +++ b/apps/hub/app/(app)/settings/page.tsx @@ -40,7 +40,7 @@ export default async function Page() {

- +
diff --git a/apps/hub/app/(auth)/email-verification/action.ts b/apps/hub/app/(auth)/email-verification/action.ts new file mode 100644 index 00000000..dc3c2a41 --- /dev/null +++ b/apps/hub/app/(auth)/email-verification/action.ts @@ -0,0 +1,39 @@ +"use server"; + +import { prisma } from "@repo/db"; + +export const checkEmailCode = async (code: string) => { + const users = await prisma.user.findMany({ + where: { + emailVerificationToken: code, + }, + select: { + id: true, + emailVerificationToken: true, + emailVerificationExpiresAt: true, + }, + }); + const user = users[0]; + + if (!user || !user.emailVerificationExpiresAt) { + return { error: "Code ist ungültig" }; + } + + if (user.emailVerificationExpiresAt < new Date()) { + return { error: "Code ist nicht mehr gültig" }; + } + + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + emailVerified: true, + emailVerificationToken: null, + emailVerificationExpiresAt: null, + }, + }); + return { + message: "Email bestätigt!", + }; +}; diff --git a/apps/hub/app/(auth)/email-verification/page.tsx b/apps/hub/app/(auth)/email-verification/page.tsx index a478e812..97878e88 100644 --- a/apps/hub/app/(auth)/email-verification/page.tsx +++ b/apps/hub/app/(auth)/email-verification/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { checkEmailCode } from "(app)/admin/user/action"; +import { checkEmailCode } from "(auth)/email-verification/action"; import { Check } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; diff --git a/apps/hub/helper/mail.ts b/apps/hub/helper/mail.ts index a191297b..74ac9fad 100644 --- a/apps/hub/helper/mail.ts +++ b/apps/hub/helper/mail.ts @@ -13,11 +13,16 @@ export const sendMail = async (email: string, subject: string, html: string) => export const sendMailByTemplate = async ( email: string, - template: "password-change" | "course-completed" | "email-verification", + template: + | "password-change" + | "course-completed" + | "email-verification" + | "ban-notice" + | "timeban-notice", data: any, ) => { try { - const res = await fetch(`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/mail/template/${template}`, { + await fetch(`${process.env.NEXT_PUBLIC_HUB_SERVER_URL}/mail/template/${template}`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -26,7 +31,6 @@ export const sendMailByTemplate = async ( data, }), }); - console.log(res); } catch (error) { console.error("Error sending mail:", error); } diff --git a/packages/database/prisma/json/User.ts b/packages/database/prisma/json/User.ts index 4c364b20..0603e0f9 100644 --- a/packages/database/prisma/json/User.ts +++ b/packages/database/prisma/json/User.ts @@ -21,19 +21,14 @@ export const getPublicUser = ( ignorePrivacy: false, }, ): PublicUser => { + const lastName = user.lastname + .split(" ") + .map((part) => `${part[0]}.`) + .join(" "); return { firstname: user.firstname, - lastname: - user.settingsHideLastname && !options.ignorePrivacy - ? "" - : user.lastname - .split(" ") - .map((part) => `${part[0]}.`) - .join(" "), // Only take the first part of the name - fullName: `${user.firstname} ${user.lastname - .split(" ") - .map((part) => `${part[0]}.`) - .join(" ")}`, + lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName, // Only take the first letter of each section of the last name + fullName: `${user.firstname} ${lastName}`, publicId: user.publicId, badges: user.badges, };