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 = () => {
const modalRef = useRef<HTMLDialogElement>(null);
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");
useEffect(() => {
@@ -105,12 +107,28 @@ export const FirstPath = () => {
return (
<dialog ref={modalRef} className="modal">
<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">
{session?.user.migratedFromV1
? "Hallo, Hier hat sich einiges geändert!"
: "Wähle deinen Einstieg!"}
</h3>
<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">
Willkommen bei Virtual Air Rescue!
<br /> Wie möchtest du bei uns starten? Du kannst später jederzeit auch den anderen Pfad
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">
{page === "path" && <PathsOptions selected={selected} setSelected={setSelected} />}
{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";
import { Check, Eye, ShieldQuestion, X } from "lucide-react";
import Link from "next/link";
import { PaginatedTable } from "_components/PaginatedTable";
import { Report, User } from "@repo/db";
import { ColumnDef } from "@tanstack/react-table";
import { Workflow, Plane } from "lucide-react";
import { reportColumns } from "(app)/admin/report/columns";
export default function ReportPage() {
return (
@@ -15,72 +11,7 @@ export default function ReportPage() {
Sender: true,
Reported: true,
}}
columns={
[
{
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>[]
}
columns={reportColumns}
/>
);
}

View File

@@ -58,6 +58,7 @@ import { setStandardName } from "../../../../../../helper/discord";
import { penaltyColumns } from "(app)/admin/penalty/columns";
import { PenaltyDropdown } from "(app)/admin/user/[id]/_components/AddPenaltyDropdown";
import { addPenalty, editPenalty, editPenaltys } from "(app)/admin/penalty/actions";
import { reportColumns } from "(app)/admin/report/columns";
interface ProfileFormProps {
user: User;
@@ -405,50 +406,7 @@ export const UserReports = ({ user }: { user: User }) => {
Sender: true,
Reported: true,
}}
columns={
[
{
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>[]
}
columns={reportColumns}
/>
</div>
);

View File

@@ -32,7 +32,7 @@ export const ProfileForm = ({
user: User;
penaltys: Penalty[];
}): React.JSX.Element => {
const canEdit = penaltys.length === 0 && user.isBanned;
const canEdit = penaltys.length === 0 && !user.isBanned;
const schema = z.object({
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"
onClick={async () => {
await deleteUser(user.id);
router.push("/login");
router.push("/logout");
}}
>
<Trash2 size={15} /> Konto sofort löschen

View File

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

View File

@@ -1,8 +1,10 @@
import { AuthOptions, getServerSession as getNextAuthServerSession } from "next-auth";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import Credentials from "next-auth/providers/credentials";
import { prisma } from "@repo/db";
import { DiscordAccount, prisma, User } from "@repo/db";
import bcrypt from "bcryptjs";
import oldUser from "./var.User.json";
import { OldUser } from "../../../../types/oldUser";
export const options: AuthOptions = {
providers: [
@@ -14,9 +16,59 @@ export const options: AuthOptions = {
async authorize(credentials) {
try {
if (!credentials) throw new Error("No credentials provided");
const user = await prisma.user.findFirstOrThrow({
const user = await prisma.user.findFirst({
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)) {
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
// relations:
User User @relation(fields: [userId], references: [id])
Mission Mission @relation(fields: [missionId], references: [id])
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
Mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade)
Station Station @relation(fields: [stationId], references: [id])
@@unique([userId, missionId, stationId])
@@ -57,7 +57,7 @@ model MissionsOnStations {
stationId Int
// relations:
Mission Mission @relation(fields: [missionId], references: [id])
Mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade)
Station Station @relation(fields: [stationId], references: [id])
@@id([missionId, stationId])

View File

@@ -6,6 +6,7 @@ enum BADGES {
D2
D3
DAY1
V1Veteran
}
enum PERMISSION {
@@ -34,6 +35,7 @@ model User {
// Settings:
pathSelected Boolean @default(false)
migratedFromV1 Boolean @default(false)
settingsNtfyRoom String? @map(name: "settings_ntfy_room")
settingsMicDevice String? @map(name: "settings_mic_device")
settingsMicVolume Float? @map(name: "settings_mic_volume")
@@ -83,14 +85,14 @@ model DiscordAccount {
avatar String? @map(name: "avatar")
globalName String @map(name: "global_name")
verified Boolean @default(false)
accessToken String @map(name: "access_token")
accessToken String? @map(name: "access_token")
refreshToken String @map(name: "refresh_token")
tokenType String @map(name: "token_type")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
// 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")
}