Account migration funktioniert nun
This commit is contained in:
@@ -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" && (
|
||||
|
||||
70
apps/hub/app/(app)/admin/report/columns.tsx
Normal file
70
apps/hub/app/(app)/admin/report/columns.tsx
Normal 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>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
749736
apps/hub/app/api/auth/[...nextauth]/var.User.json
Normal file
749736
apps/hub/app/api/auth/[...nextauth]/var.User.json
Normal file
File diff suppressed because it is too large
Load Diff
26
apps/hub/types/oldUser.ts
Normal file
26
apps/hub/types/oldUser.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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])
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user