Account migration funktioniert nun

This commit is contained in:
PxlLoewe
2025-06-26 01:01:42 -07:00
parent 2bd8a455c8
commit d2ebea7fc2
11 changed files with 749926 additions and 138 deletions

View File

@@ -93,7 +93,9 @@ const EventSelect = ({ pathSelected }: { pathSelected: "disponent" | "pilot" })
export const FirstPath = () => { export const FirstPath = () => {
const modalRef = useRef<HTMLDialogElement>(null); const modalRef = useRef<HTMLDialogElement>(null);
const { data: session } = useSession(); const { data: session } = useSession();
const [selected, setSelected] = useState<"disponent" | "pilot" | null>(null); const [selected, setSelected] = useState<"disponent" | "pilot" | null>(
session?.user.badges.includes("D1") ? "disponent" : null,
);
const [page, setPage] = useState<"path" | "event-select">("path"); const [page, setPage] = useState<"path" | "event-select">("path");
useEffect(() => { useEffect(() => {
@@ -105,12 +107,28 @@ export const FirstPath = () => {
return ( return (
<dialog ref={modalRef} className="modal"> <dialog ref={modalRef} className="modal">
<div className="modal-box w-11/12 max-w-5xl"> <div className="modal-box w-11/12 max-w-5xl">
<h3 className="flex items-center gap-2 text-lg font-bold mb-10">Wähle deinen Einstieg!</h3> <h3 className="flex items-center gap-2 text-lg font-bold mb-10">
<p className="mb-8 text-base text-base-content/80 text-center"> {session?.user.migratedFromV1
Willkommen bei Virtual Air Rescue! ? "Hallo, Hier hat sich einiges geändert!"
<br /> Wie möchtest du bei uns starten? Du kannst später jederzeit auch den anderen Pfad : "Wähle deinen Einstieg!"}
ausprobieren, wenn du möchtest. </h3>
</p> <h2 className="text-2xl font-bold mb-4 text-center">Willkommen bei Virtual Air Rescue!</h2>
{session?.user.migratedFromV1 ? (
<p className="mb-8 text-base text-base-content/80 text-center">
Dein Account wurde erfolgreich auf das neue System migriert. Herzlich wilkommen im neuen
HUB! Um die Erfahrung für alle Nutzer zu steigern haben wir uns dazu entschlossen, dass
alle Nutzer einen Test absolvieren müssen:{" "}
{session.user.badges.includes("D1") &&
`Da du vorher schon den D1-Test absolviert hast, kannst du unter Disponent das Quick-Lane Event auswähen. Um Pilot zu werden kannst du dann später den Piloten-Kurs absolvieren.`}
{(!session.user.badges.includes("D1") || session.user.badges.includes("P1")) &&
`Als Pilot musst du den Piloten-Test abschließen.`}
</p>
) : (
<p>
Wie möchtest du bei uns starten? Du kannst später jederzeit auch den anderen Pfad
ausprobieren, wenn du möchtest.
</p>
)}
<div className="flex flex-col items-center justify-center m-20"> <div className="flex flex-col items-center justify-center m-20">
{page === "path" && <PathsOptions selected={selected} setSelected={setSelected} />} {page === "path" && <PathsOptions selected={selected} setSelected={setSelected} />}
{page === "event-select" && ( {page === "event-select" && (

View File

@@ -0,0 +1,70 @@
import { Report, User } from "@repo/db";
import { ColumnDef } from "@tanstack/react-table";
import { Check, Eye, Plane, ShieldQuestion, Workflow, X } from "lucide-react";
import Link from "next/link";
export const reportColumns: ColumnDef<Report & { Sender?: User; Reported: User }>[] = [
{
accessorKey: "reviewed",
header: "Erledigt",
cell: ({ row }) => {
return (
<div className="text-center">
{row.getValue("reviewed") ? (
<Check className="text-green-500 w-5 h-5" />
) : (
<X className="text-red-500 w-5 h-5" />
)}
</div>
);
},
},
{
accessorKey: "Sender",
header: "Sender",
cell: ({ row }) => {
const user = row.original.Sender;
if (!user) return "Unbekannt";
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "reportedUserRole",
header: "Rolle des gemeldeten Nutzers",
cell: ({ row }) => {
const role = row.getValue("reportedUserRole") as string | undefined;
const Icon = role ? (role.startsWith("LST") ? Workflow : Plane) : ShieldQuestion;
return (
<span className="flex items-center gap-2">
<Icon className="w-4 h-4" />
{role || "Unbekannt"}
</span>
);
},
},
{
accessorKey: "Reported",
header: "Reported",
cell: ({ row }) => {
const user = row.original.Reported;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) => new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => (
<Link href={`/admin/report/${row.original.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
),
},
];

View File

@@ -1,10 +1,6 @@
"use client"; "use client";
import { Check, Eye, ShieldQuestion, X } from "lucide-react";
import Link from "next/link";
import { PaginatedTable } from "_components/PaginatedTable"; import { PaginatedTable } from "_components/PaginatedTable";
import { Report, User } from "@repo/db"; import { reportColumns } from "(app)/admin/report/columns";
import { ColumnDef } from "@tanstack/react-table";
import { Workflow, Plane } from "lucide-react";
export default function ReportPage() { export default function ReportPage() {
return ( return (
@@ -15,72 +11,7 @@ export default function ReportPage() {
Sender: true, Sender: true,
Reported: true, Reported: true,
}} }}
columns={ columns={reportColumns}
[
{
accessorKey: "reviewed",
header: "Erledigt",
cell: ({ row }) => {
return (
<div className="text-center">
{row.getValue("reviewed") ? (
<Check className="text-green-500 w-5 h-5" />
) : (
<X className="text-red-500 w-5 h-5" />
)}
</div>
);
},
},
{
accessorKey: "Sender",
header: "Sender",
cell: ({ row }) => {
const user = row.getValue("Sender") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "reportedUserRole",
header: "Rolle des gemeldeten Nutzers",
cell: ({ row }) => {
const role = row.getValue("reportedUserRole") as string | undefined;
const Icon = role ? (role.startsWith("LST") ? Workflow : Plane) : ShieldQuestion;
return (
<span className="flex items-center gap-2">
<Icon className="w-4 h-4" />
{role || "Unbekannt"}
</span>
);
},
},
{
accessorKey: "Reported",
header: "Reported",
cell: ({ row }) => {
const user = row.getValue("Reported") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) => new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => (
<Link href={`/admin/report/${row.original.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
),
},
] as ColumnDef<Report>[]
}
/> />
); );
} }

View File

@@ -58,6 +58,7 @@ import { setStandardName } from "../../../../../../helper/discord";
import { penaltyColumns } from "(app)/admin/penalty/columns"; import { penaltyColumns } from "(app)/admin/penalty/columns";
import { PenaltyDropdown } from "(app)/admin/user/[id]/_components/AddPenaltyDropdown"; import { PenaltyDropdown } from "(app)/admin/user/[id]/_components/AddPenaltyDropdown";
import { addPenalty, editPenalty, editPenaltys } from "(app)/admin/penalty/actions"; import { addPenalty, editPenalty, editPenaltys } from "(app)/admin/penalty/actions";
import { reportColumns } from "(app)/admin/report/columns";
interface ProfileFormProps { interface ProfileFormProps {
user: User; user: User;
@@ -405,50 +406,7 @@ export const UserReports = ({ user }: { user: User }) => {
Sender: true, Sender: true,
Reported: true, Reported: true,
}} }}
columns={ columns={reportColumns}
[
{
accessorKey: "reviewed",
header: "Erledigt",
cell: ({ row }) => {
return (
<div className="text-center">
{row.getValue("reviewed") ? (
<Check className="text-green-500 w-5 h-5" />
) : (
<X className="text-red-500 w-5 h-5" />
)}
</div>
);
},
},
{
accessorKey: "Sender",
header: "Sender",
cell: ({ row }) => {
const user = row.getValue("Sender") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) => new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => (
<Link href={`/admin/report/${row.original.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
),
},
] as ColumnDef<Report>[]
}
/> />
</div> </div>
); );

View File

@@ -32,7 +32,7 @@ export const ProfileForm = ({
user: User; user: User;
penaltys: Penalty[]; penaltys: Penalty[];
}): React.JSX.Element => { }): React.JSX.Element => {
const canEdit = penaltys.length === 0 && user.isBanned; const canEdit = penaltys.length === 0 && !user.isBanned;
const schema = z.object({ const schema = z.object({
firstname: z.string().min(2).max(30), firstname: z.string().min(2).max(30),
@@ -318,7 +318,7 @@ export const DeleteForm = ({ user, penaltys }: { user: User; penaltys: Penalty[]
className="btn-error btn-outline btn-sm w-full" className="btn-error btn-outline btn-sm w-full"
onClick={async () => { onClick={async () => {
await deleteUser(user.id); await deleteUser(user.id);
router.push("/login"); router.push("/logout");
}} }}
> >
<Trash2 size={15} /> Konto sofort löschen <Trash2 size={15} /> Konto sofort löschen

View File

@@ -16,15 +16,10 @@ const BadgeImage = {
[BADGES.D2]: D2, [BADGES.D2]: D2,
[BADGES.D3]: D3, [BADGES.D3]: D3,
[BADGES.DAY1]: DAY1, [BADGES.DAY1]: DAY1,
[BADGES.V1Veteran]: DAY1,
}; };
export const Badge = ({ export const Badge = ({ name, className }: { name: BADGES; className?: string }) => {
name,
className,
}: {
name: BADGES;
className?: string;
}) => {
const image = BadgeImage[name]; const image = BadgeImage[name];
return ( return (

View File

@@ -1,8 +1,10 @@
import { AuthOptions, getServerSession as getNextAuthServerSession } from "next-auth"; import { AuthOptions, getServerSession as getNextAuthServerSession } from "next-auth";
import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { PrismaAdapter } from "@next-auth/prisma-adapter";
import Credentials from "next-auth/providers/credentials"; import Credentials from "next-auth/providers/credentials";
import { prisma } from "@repo/db"; import { DiscordAccount, prisma, User } from "@repo/db";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import oldUser from "./var.User.json";
import { OldUser } from "../../../../types/oldUser";
export const options: AuthOptions = { export const options: AuthOptions = {
providers: [ providers: [
@@ -14,9 +16,59 @@ export const options: AuthOptions = {
async authorize(credentials) { async authorize(credentials) {
try { try {
if (!credentials) throw new Error("No credentials provided"); if (!credentials) throw new Error("No credentials provided");
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirst({
where: { email: credentials.email }, where: { email: credentials.email },
}); });
const v1User = (oldUser as OldUser[]).find((u) => u.email === credentials.email);
if (!user && v1User) {
if (bcrypt.compareSync(credentials.password, v1User.password)) {
const newUser = await prisma.user.create({
data: {
email: v1User.email,
password: v1User.password,
migratedFromV1: true,
firstname: v1User.firstname,
lastname: v1User.lastname,
publicId: v1User.publicId,
badges: [
...v1User.badges
.map((badge) => {
switch (badge) {
case "day-1-member":
return "DAY1";
case "d-1":
return "D1";
case "p-1":
return "P1";
default:
return null;
}
})
.filter((badge) => badge !== null),
"V1Veteran",
],
},
});
if (v1User.discord) {
await prisma.discordAccount.create({
data: {
tokenType: "Bearer",
refreshToken: v1User.discord.tokens.refresh_token,
discordId: v1User.discord.profile.id,
userId: newUser.id,
username: v1User.discord.profile.username,
globalName: v1User.discord.profile.global_name,
avatar: v1User.discord.profile.avatar,
email: v1User.discord.profile.email,
verified: v1User.discord.profile.verified,
},
});
}
return newUser;
}
}
if (!user) return null;
if (bcrypt.compareSync(credentials.password, user.password)) { if (bcrypt.compareSync(credentials.password, user.password)) {
return user; return user;
} }

File diff suppressed because it is too large Load Diff

26
apps/hub/types/oldUser.ts Normal file
View File

@@ -0,0 +1,26 @@
export interface OldUser {
firstname: string;
publicId: string;
badges: string[];
lastname: string;
email: string;
password: string;
settings: {
privacyHideLastnameInNickname?: boolean;
notifyRoomID?: string;
};
discord?: {
profile: {
id: string;
username: string;
global_name: string;
avatar: string;
email: string;
verified: boolean;
};
tokens: {
refresh_token: string;
scope: string;
};
};
}

View File

@@ -45,8 +45,8 @@ model MissionOnStationUsers {
stationId Int stationId Int
// relations: // relations:
User User @relation(fields: [userId], references: [id]) User User @relation(fields: [userId], references: [id], onDelete: Cascade)
Mission Mission @relation(fields: [missionId], references: [id]) Mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade)
Station Station @relation(fields: [stationId], references: [id]) Station Station @relation(fields: [stationId], references: [id])
@@unique([userId, missionId, stationId]) @@unique([userId, missionId, stationId])
@@ -57,7 +57,7 @@ model MissionsOnStations {
stationId Int stationId Int
// relations: // relations:
Mission Mission @relation(fields: [missionId], references: [id]) Mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade)
Station Station @relation(fields: [stationId], references: [id]) Station Station @relation(fields: [stationId], references: [id])
@@id([missionId, stationId]) @@id([missionId, stationId])

View File

@@ -6,6 +6,7 @@ enum BADGES {
D2 D2
D3 D3
DAY1 DAY1
V1Veteran
} }
enum PERMISSION { enum PERMISSION {
@@ -34,6 +35,7 @@ model User {
// Settings: // Settings:
pathSelected Boolean @default(false) pathSelected Boolean @default(false)
migratedFromV1 Boolean @default(false)
settingsNtfyRoom String? @map(name: "settings_ntfy_room") settingsNtfyRoom String? @map(name: "settings_ntfy_room")
settingsMicDevice String? @map(name: "settings_mic_device") settingsMicDevice String? @map(name: "settings_mic_device")
settingsMicVolume Float? @map(name: "settings_mic_volume") settingsMicVolume Float? @map(name: "settings_mic_volume")
@@ -83,14 +85,14 @@ model DiscordAccount {
avatar String? @map(name: "avatar") avatar String? @map(name: "avatar")
globalName String @map(name: "global_name") globalName String @map(name: "global_name")
verified Boolean @default(false) verified Boolean @default(false)
accessToken String @map(name: "access_token") accessToken String? @map(name: "access_token")
refreshToken String @map(name: "refresh_token") refreshToken String @map(name: "refresh_token")
tokenType String @map(name: "token_type") tokenType String @map(name: "token_type")
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")
// relations: // relations:
user User @relation(fields: [userId], references: [id]) // Beziehung zu User user User @relation(fields: [userId], references: [id], onDelete: Cascade) // Beziehung zu User
@@map(name: "discord_accounts") @@map(name: "discord_accounts")
} }