mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
Merge pull request #57 from Since-3/feature/employee-onboarding-process
added employee onboarding process and access permission
This commit is contained in:
commit
13da24a411
20 changed files with 913 additions and 56 deletions
|
|
@ -1,7 +1,18 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "api.dicebear.com",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "dlvlsahpuahlymvemzkl.supabase.co",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import EmployeeOnboardingView from "@/src/modules/auth/ui/views/employee-onboaring-view";
|
||||
|
||||
const Onboarding = () => {
|
||||
return <EmployeeOnboardingView />;
|
||||
};
|
||||
|
||||
export default Onboarding;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
|
||||
import CourseCreateView from "@/src/modules/courses/ui/views/course-create-view";
|
||||
import { getMySportTypes } from "@/src/modules/courses/actions/course-actions";
|
||||
|
||||
const CourseCreate = async () => {
|
||||
await requireManager();
|
||||
await requireManagerOrPermission("canCreateCourses");
|
||||
|
||||
const sportsResult = await getMySportTypes();
|
||||
const customSports = sportsResult.success ? sportsResult.sports : [];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
|
||||
import CourseEditView from "@/src/modules/courses/ui/views/course-edit-view";
|
||||
import { getMySportTypes } from "@/src/modules/courses/actions/course-actions";
|
||||
|
||||
const CourseEdit = async ({ params }: { params: Promise<{ id: string }> }) => {
|
||||
await requireManager();
|
||||
await requireManagerOrPermission("canCreateCourses");
|
||||
const { id } = await params;
|
||||
|
||||
const sportsResult = await getMySportTypes();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
|
||||
import CourseView from "@/src/modules/courses/ui/views/course-view";
|
||||
|
||||
const Course = async () => {
|
||||
await requireManager();
|
||||
await requireManagerOrPermission("canCreateCourses");
|
||||
return <CourseView />;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
|
||||
import EmployeeCreateView from "@/src/modules/employee/ui/views/employee-create-view";
|
||||
import { getMyEmployeeRoles } from "@/src/modules/employee/actions/employee-actions";
|
||||
|
||||
const EmployeeCreate = async () => {
|
||||
await requireManager();
|
||||
await requireManagerOrPermission("canCreateEmployees");
|
||||
|
||||
// Lade alle bereits verwendeten custom Rollen
|
||||
const rolesResult = await getMyEmployeeRoles();
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
const Onboarding = () => {
|
||||
return <div>Onboarding Employee</div>;
|
||||
};
|
||||
|
||||
export default Onboarding;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { requireManagerOrPermission } from "@/src/lib/auth/getUser";
|
||||
import EmployeeView from "@/src/modules/employee/ui/views/employee-view";
|
||||
|
||||
const Employee = async () => {
|
||||
await requireManager();
|
||||
await requireManagerOrPermission("canCreateEmployees");
|
||||
return <EmployeeView />;
|
||||
};
|
||||
|
||||
|
|
|
|||
44
src/app/api/auth/register/employee/route.ts
Normal file
44
src/app/api/auth/register/employee/route.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, password, firstName, lastName, tel, gender, qualification } = body;
|
||||
|
||||
if (!email || !password || !firstName || !lastName) {
|
||||
return NextResponse.json({ error: "Pflichtfelder fehlen" }, { status: 400 });
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
firstName,
|
||||
lastName,
|
||||
tel,
|
||||
gender,
|
||||
qualification,
|
||||
role: "employee",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Supabase Auth Error:", error.message);
|
||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!data.user) {
|
||||
return NextResponse.json({ error: "Account konnte nicht erstellt werden" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error in /api/auth/register/employee:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
52
src/app/api/upload/avatar/route.ts
Normal file
52
src/app/api/upload/avatar/route.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { createAdminClient } from "@/src/lib/supabase/admin";
|
||||
|
||||
/**
|
||||
* POST /api/upload/avatar
|
||||
* Öffentliche Route für den Bild-Upload während des Onboardings.
|
||||
* Nutzt den Service Role Key um RLS zu bypassen.
|
||||
* Erwartet: multipart/form-data mit "file" und "path" Feldern.
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
const path = formData.get("path") as string | null;
|
||||
|
||||
if (!file || !path) {
|
||||
return NextResponse.json({ error: "Datei und Pfad erforderlich" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
return NextResponse.json({ error: "Nur Bilddateien erlaubt" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return NextResponse.json({ error: "Datei darf maximal 5MB groß sein" }, { status: 400 });
|
||||
}
|
||||
|
||||
const adminClient = createAdminClient();
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const { data, error } = await adminClient.storage.from("avatars").upload(path, buffer, {
|
||||
contentType: file.type,
|
||||
upsert: true,
|
||||
cacheControl: "3600",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Storage upload error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = adminClient.storage.from("avatars").getPublicUrl(data.path);
|
||||
|
||||
return NextResponse.json({ publicUrl }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error in /api/upload/avatar:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
99
src/components/layout/ImageUploadComponent.tsx
Normal file
99
src/components/layout/ImageUploadComponent.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { Camera, X } from "lucide-react";
|
||||
|
||||
interface ImageUploadComponentProps {
|
||||
value: File | null;
|
||||
preview: string | null;
|
||||
onChange: (file: File | null, preview: string | null) => void;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const ImageUploadComponent = ({ value, preview, onChange, error }: ImageUploadComponentProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
if (!file.type.startsWith("image/")) return;
|
||||
const url = URL.createObjectURL(file);
|
||||
onChange(file, url);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
const handleRemove = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onChange(null, null);
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div
|
||||
onClick={() => !preview && inputRef.current?.click()}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
className={`relative w-28 h-28 rounded-full border-2 border-dashed flex items-center justify-center cursor-pointer transition-colors overflow-hidden
|
||||
${isDragging ? "border-blue bg-blue/5" : "border-gray-300 hover:border-blue hover:bg-gray-50"}
|
||||
${preview ? "border-solid border-blue cursor-default" : ""}
|
||||
`}
|
||||
>
|
||||
{preview ? (
|
||||
<>
|
||||
<Image src={preview} alt="Profilbild Vorschau" fill className="object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemove}
|
||||
className="absolute top-1 right-1 bg-white rounded-full p-0.5 shadow hover:bg-red-50 transition-colors z-10"
|
||||
>
|
||||
<X size={14} className="text-red-500" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-1 text-gray-400">
|
||||
<Camera size={28} />
|
||||
<span className="text-xs text-center leading-tight px-2">Foto hochladen</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!preview && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="text-sm text-blue underline cursor-pointer"
|
||||
>
|
||||
Bild auswählen
|
||||
</button>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploadComponent;
|
||||
|
|
@ -35,7 +35,22 @@ export type ManagerData = {
|
|||
}>;
|
||||
};
|
||||
|
||||
export type AuthUserData = UserData | ManagerData;
|
||||
export type EmployeeData = {
|
||||
id: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
email: string;
|
||||
qualification: string | null;
|
||||
pbSrc: string | null;
|
||||
createdAt: Date | null;
|
||||
role: "employee";
|
||||
permissions: {
|
||||
canCreateCourses: boolean;
|
||||
canCreateEmployees: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type AuthUserData = UserData | ManagerData | EmployeeData;
|
||||
|
||||
// Note: We don't use in-memory caching because this app runs on Vercel (serverless)
|
||||
// Serverless functions are stateless - in-memory cache would be:
|
||||
|
|
@ -137,7 +152,43 @@ export const getUserData = cache(async (): Promise<AuthUserData | null> => {
|
|||
return managerWithRole;
|
||||
}
|
||||
|
||||
console.log("⚠️ [DB] No user or manager found for:", user.email);
|
||||
// Try Employee table — match by email since Supabase ID is not stored in Employee
|
||||
const employeeData = await prisma.employee.findUnique({
|
||||
where: { email: user.email! },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
qualification: true,
|
||||
pbSrc: true,
|
||||
permissions: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (employeeData) {
|
||||
console.log(
|
||||
"✅ [DB] Employee data fetched:",
|
||||
`${employeeData.firstName} ${employeeData.lastName}`
|
||||
);
|
||||
const rawPermissions = employeeData.permissions as {
|
||||
canCreateCourses?: boolean;
|
||||
canCreateEmployees?: boolean;
|
||||
} | null;
|
||||
|
||||
const employeeWithRole: EmployeeData = {
|
||||
...employeeData,
|
||||
role: "employee",
|
||||
permissions: {
|
||||
canCreateCourses: rawPermissions?.canCreateCourses ?? false,
|
||||
canCreateEmployees: rawPermissions?.canCreateEmployees ?? false,
|
||||
},
|
||||
};
|
||||
return employeeWithRole;
|
||||
}
|
||||
|
||||
console.log("⚠️ [DB] No user, manager or employee found for:", user.email);
|
||||
return null;
|
||||
});
|
||||
|
||||
|
|
@ -194,3 +245,23 @@ export function isManager(userData: AuthUserData): userData is ManagerData {
|
|||
export function isUser(userData: AuthUserData): userData is UserData {
|
||||
return userData.role === "user";
|
||||
}
|
||||
|
||||
// Helper function to check if user is an employee
|
||||
export function isEmployee(userData: AuthUserData): userData is EmployeeData {
|
||||
return userData.role === "employee";
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires that the authenticated user is a Manager OR an Employee with the given permission.
|
||||
* Redirects to /dashboard if access is denied.
|
||||
*/
|
||||
export async function requireManagerOrPermission(permission: keyof EmployeeData["permissions"]) {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
if (isManager(userData)) return userData;
|
||||
|
||||
if (isEmployee(userData) && userData.permissions[permission]) return userData;
|
||||
|
||||
const { redirect } = await import("next/navigation");
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
|
|
|||
21
src/lib/supabase/admin.ts
Normal file
21
src/lib/supabase/admin.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
/**
|
||||
* Supabase Admin Client — nutzt den Service Role Key (Bypass Row Level Security).
|
||||
* Nur serverseitig verwenden, niemals im Client-Code.
|
||||
*/
|
||||
export function createAdminClient() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (!url || !serviceRoleKey) {
|
||||
throw new Error("SUPABASE_SERVICE_ROLE_KEY ist nicht gesetzt");
|
||||
}
|
||||
|
||||
return createClient(url, serviceRoleKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
293
src/modules/auth/ui/views/employee-onboaring-view.tsx
Normal file
293
src/modules/auth/ui/views/employee-onboaring-view.tsx
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ZodError, z } from "zod";
|
||||
import AuthSidebarComponent from "../components/AuthSidebarComponent";
|
||||
import InputComponent from "@/src/components/layout/InputComponent";
|
||||
import PhoneInputComponent from "@/src/components/layout/PhoneInputComponent";
|
||||
import DropdownComponent from "@/src/components/layout/DropdownComponent";
|
||||
import ImageUploadComponent from "@/src/components/layout/ImageUploadComponent";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Spinner } from "@/src/components/ui/spinner";
|
||||
import {
|
||||
getEmployeeByOnboardingToken,
|
||||
completeEmployeeOnboarding,
|
||||
} from "@/src/modules/employee/actions/employee-actions";
|
||||
import { generateFilePath } from "@/src/lib/supabase/storage";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const step1Schema = z
|
||||
.object({
|
||||
firstName: z.string().min(2, "Vorname ist zu kurz"),
|
||||
lastName: z.string().min(2, "Nachname ist zu kurz"),
|
||||
tel: z.string().min(5, "Telefonnummer ist erforderlich"),
|
||||
password: z
|
||||
.string()
|
||||
.min(8)
|
||||
.regex(
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[.@$!%*?&])[A-Za-z\d.@$!%*?&]+$/,
|
||||
"Passwort muss mindestens 8 Zeichen, Groß-/Kleinbuchstaben, eine Zahl und ein Sonderzeichen enthalten"
|
||||
),
|
||||
passwordConfirm: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.passwordConfirm, {
|
||||
message: "Passwörter stimmen nicht überein",
|
||||
path: ["passwordConfirm"],
|
||||
});
|
||||
|
||||
const step2Schema = z.object({
|
||||
gender: z
|
||||
.enum(["Men", "Woman", "Various"])
|
||||
.or(z.literal(""))
|
||||
.refine((val) => val !== "", { message: "Geschlecht ist erforderlich" }),
|
||||
qualification: z.string().min(1, "Qualifikation ist erforderlich"),
|
||||
});
|
||||
|
||||
const EmployeeOnboardingView = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const token = params?.onboardingToken as string;
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tokenLoading, setTokenLoading] = useState(true);
|
||||
const [tokenError, setTokenError] = useState<string | null>(null);
|
||||
const [employeeEmail, setEmployeeEmail] = useState("");
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [tel, setTel] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||
const [gender, setGender] = useState("");
|
||||
const [qualification, setQualification] = useState("");
|
||||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkToken = async () => {
|
||||
if (!token) {
|
||||
setTokenError("Kein Onboarding-Token gefunden");
|
||||
setTokenLoading(false);
|
||||
return;
|
||||
}
|
||||
const result = await getEmployeeByOnboardingToken(token);
|
||||
if (!result.success) {
|
||||
setTokenError(result.error || "Ungültiger Link");
|
||||
} else {
|
||||
setEmployeeEmail(result.employee!.email);
|
||||
}
|
||||
setTokenLoading(false);
|
||||
};
|
||||
checkToken();
|
||||
}, [token]);
|
||||
|
||||
const handleNext = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
try {
|
||||
step1Schema.parse({ firstName, lastName, tel, password, passwordConfirm });
|
||||
setStep(2);
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
const path = issue.path[0];
|
||||
if (typeof path === "string") fieldErrors[path] = issue.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
try {
|
||||
step2Schema.parse({ gender, qualification });
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
const path = issue.path[0];
|
||||
if (typeof path === "string") fieldErrors[path] = issue.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
let pbSrc: string | undefined;
|
||||
if (avatarFile) {
|
||||
try {
|
||||
const filePath = generateFilePath(token, avatarFile.name, "employees");
|
||||
const formData = new FormData();
|
||||
formData.append("file", avatarFile);
|
||||
formData.append("path", filePath);
|
||||
const res = await fetch("/api/upload/avatar", { method: "POST", body: formData });
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json.error);
|
||||
pbSrc = json.publicUrl;
|
||||
} catch {
|
||||
toast.error("Fehler beim Hochladen des Profilbilds");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await completeEmployeeOnboarding(token, {
|
||||
firstName,
|
||||
lastName,
|
||||
tel,
|
||||
password,
|
||||
gender,
|
||||
qualification,
|
||||
pbSrc,
|
||||
});
|
||||
setLoading(false);
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Account erfolgreich erstellt! Du kannst dich jetzt einloggen.");
|
||||
router.push("/login");
|
||||
} else {
|
||||
toast.error(result.error || "Ein Fehler ist aufgetreten");
|
||||
}
|
||||
};
|
||||
|
||||
if (tokenLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
return (
|
||||
<div className="flex min-h-screen justify-center items-center flex-col gap-4 px-4">
|
||||
<h1 className="text-2xl font-bold text-red-500">{tokenError}</h1>
|
||||
<p className="text-gray-500 text-center">
|
||||
Bitte wende dich an deinen Manager für einen neuen Onboarding-Link.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<div className="hidden lg:block lg:w-[40%]">
|
||||
<div className="fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
|
||||
<AuthSidebarComponent
|
||||
title="Werde Teil unseres Teams"
|
||||
footer="info@since3.de"
|
||||
isSteps
|
||||
step={step}
|
||||
totalSteps={2}
|
||||
onStepChange={setStep}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-[60%] lg:ml-auto flex justify-center items-center min-h-screen px-4">
|
||||
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
|
||||
<h1 className="text-2xl font-extrabold text-blue mb-2 mt-8">Herzlich Willkommen!</h1>
|
||||
<p className="text-gray-500 mb-6">{employeeEmail}</p>
|
||||
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleNext} className="space-y-5">
|
||||
<div className="flex gap-4">
|
||||
<InputComponent
|
||||
isLabel
|
||||
label="Vorname"
|
||||
type="text"
|
||||
id="onboarding-firstName"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
error={errors.firstName}
|
||||
/>
|
||||
<InputComponent
|
||||
isLabel
|
||||
label="Nachname"
|
||||
type="text"
|
||||
id="onboarding-lastName"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
</div>
|
||||
<PhoneInputComponent
|
||||
isLabel
|
||||
label="Telefonnummer"
|
||||
id="onboarding-tel"
|
||||
value={tel}
|
||||
onChange={(val) => setTel(val)}
|
||||
error={errors.tel}
|
||||
/>
|
||||
<InputComponent
|
||||
isLabel
|
||||
label="Passwort"
|
||||
type="password"
|
||||
id="onboarding-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
error={errors.password}
|
||||
/>
|
||||
<InputComponent
|
||||
isLabel
|
||||
label="Passwort bestätigen"
|
||||
type="password"
|
||||
id="onboarding-passwordConfirm"
|
||||
value={passwordConfirm}
|
||||
onChange={(e) => setPasswordConfirm(e.target.value)}
|
||||
error={errors.passwordConfirm}
|
||||
/>
|
||||
<Button className="w-full mt-6" type="submit">
|
||||
Weiter
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="text-sm text-gray-500 mb-3">Profilbild (optional)</p>
|
||||
<ImageUploadComponent
|
||||
value={avatarFile}
|
||||
preview={avatarPreview}
|
||||
onChange={(file, preview) => {
|
||||
setAvatarFile(file);
|
||||
setAvatarPreview(preview);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DropdownComponent
|
||||
label="Geschlecht"
|
||||
selected={gender}
|
||||
onSelect={(value: string) => setGender(value)}
|
||||
error={errors.gender}
|
||||
/>
|
||||
<InputComponent
|
||||
isLabel
|
||||
label="Qualifikation"
|
||||
type="text"
|
||||
id="onboarding-qualification"
|
||||
value={qualification}
|
||||
onChange={(e) => setQualification(e.target.value)}
|
||||
error={errors.qualification}
|
||||
/>
|
||||
<Button type="submit" disabled={loading} className="w-full mt-6">
|
||||
{loading ? <Spinner /> : "Account erstellen"}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeOnboardingView;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import { HelpCircle, Settings, Plus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import type { AuthUserData } from "@/src/lib/auth/getUser";
|
||||
import {
|
||||
|
|
@ -25,6 +26,26 @@ const footerMenu = [
|
|||
},
|
||||
];
|
||||
|
||||
const getDiceBearUrl = (seed: string) =>
|
||||
`https://api.dicebear.com/9.x/thumbs/svg?seed=${encodeURIComponent(seed)}`;
|
||||
|
||||
const AvatarImage = ({
|
||||
userData,
|
||||
displayName,
|
||||
}: {
|
||||
userData: AuthUserData;
|
||||
displayName: string;
|
||||
}) => {
|
||||
const pbSrc = "pbSrc" in userData ? userData.pbSrc : null;
|
||||
const src = pbSrc || getDiceBearUrl(displayName);
|
||||
|
||||
return (
|
||||
<div className="relative w-10 h-10 rounded-full overflow-hidden shrink-0 bg-gray-100">
|
||||
<Image src={src} alt={displayName} fill className="object-cover" unoptimized />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DashboardFooterProps {
|
||||
displayName: string;
|
||||
userData: AuthUserData;
|
||||
|
|
@ -91,10 +112,10 @@ const DashboardFooter = ({ displayName, userData }: DashboardFooterProps) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* DisplayName */}
|
||||
{/* DisplayName + Avatar */}
|
||||
<div className="w-full flex items-center gap-4 p-2 mt-2">
|
||||
<div className="w-10 h-10 bg-black rounded-full" />
|
||||
<p>{displayName}</p>
|
||||
<AvatarImage userData={userData} displayName={displayName} />
|
||||
<p className="truncate text-sm font-medium">{displayName}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,14 +26,22 @@ import { usePathname } from "next/navigation";
|
|||
import Logo from "@/src/public/logo-dark.png";
|
||||
import { cn } from "@/src/lib/utils";
|
||||
import DashboardFooter from "./DashboardFooter";
|
||||
import type { AuthUserData } from "@/src/lib/auth/getUser";
|
||||
import type { AuthUserData, EmployeeData } from "@/src/lib/auth/getUser";
|
||||
|
||||
const allMenuItems = [
|
||||
type MenuItem = {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
href: string;
|
||||
roles?: string[];
|
||||
permission?: keyof EmployeeData["permissions"];
|
||||
};
|
||||
|
||||
const allMenuItems: MenuItem[] = [
|
||||
{
|
||||
icon: LayoutDashboard,
|
||||
label: "Dashboard",
|
||||
href: "/dashboard",
|
||||
roles: ["manager", "user"],
|
||||
roles: ["manager", "user", "employee"],
|
||||
},
|
||||
{
|
||||
icon: Dumbbell,
|
||||
|
|
@ -47,12 +55,26 @@ const allMenuItems = [
|
|||
href: "/courses",
|
||||
roles: ["manager"],
|
||||
},
|
||||
{
|
||||
icon: Dumbbell,
|
||||
label: "Kurse",
|
||||
href: "/courses",
|
||||
roles: ["employee"],
|
||||
permission: "canCreateCourses",
|
||||
},
|
||||
{
|
||||
icon: IdCardLanyard,
|
||||
label: "Mitarbeiter",
|
||||
href: "/employee",
|
||||
roles: ["manager"],
|
||||
},
|
||||
{
|
||||
icon: IdCardLanyard,
|
||||
label: "Mitarbeiter",
|
||||
href: "/employee",
|
||||
roles: ["employee"],
|
||||
permission: "canCreateEmployees",
|
||||
},
|
||||
{
|
||||
icon: User,
|
||||
label: "Kunden",
|
||||
|
|
@ -88,8 +110,14 @@ const DashboardSidebar = ({ userData, displayName }: DashboardSidebarProps) => {
|
|||
const pathname = usePathname();
|
||||
const userRole = userData.role;
|
||||
|
||||
// Filter menu items based on user role
|
||||
const menu = allMenuItems.filter((item) => item.roles.includes(userRole));
|
||||
const menu = allMenuItems.filter((item) => {
|
||||
if (!item.roles?.includes(userRole)) return false;
|
||||
if (userRole === "employee" && item.permission) {
|
||||
const employeeData = userData as EmployeeData;
|
||||
return employeeData.permissions[item.permission] === true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
|
|
@ -104,7 +132,7 @@ const DashboardSidebar = ({ userData, displayName }: DashboardSidebarProps) => {
|
|||
<SidebarGroupContent>
|
||||
<SidebarMenu className="mt-10">
|
||||
{menu.map((item) => (
|
||||
<SidebarMenuItem key={item.href}>
|
||||
<SidebarMenuItem key={item.href + item.label}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { requireAuthWithData, isManager } from "@/src/lib/auth/getUser";
|
||||
import { requireAuthWithData, isManager, isEmployee } from "@/src/lib/auth/getUser";
|
||||
import DashboardSidebar from "./DashboardSidebar";
|
||||
|
||||
const DashboardSidebarWrapper = async () => {
|
||||
const userData = await requireAuthWithData();
|
||||
const displayName = isManager(userData)
|
||||
? `${userData.firstName} ${userData.lastName}`
|
||||
: userData.name || userData.email;
|
||||
: isEmployee(userData)
|
||||
? `${userData.firstName ?? ""} ${userData.lastName ?? ""}`.trim() || userData.email
|
||||
: userData.name || userData.email;
|
||||
|
||||
return <DashboardSidebar userData={userData} displayName={displayName} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { LogoutButton } from "@/src/components/layout/LogoutButton";
|
||||
import { isManager, requireAuthWithData, isUser } from "@/src/lib/auth/getUser";
|
||||
import { isManager, isEmployee, requireAuthWithData, isUser } from "@/src/lib/auth/getUser";
|
||||
|
||||
const DashboardView = async () => {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
// Get display name based on user type
|
||||
const displayName = isManager(userData)
|
||||
? `${userData.firstName} ${userData.lastName}`
|
||||
: userData.name || userData.email;
|
||||
: isEmployee(userData)
|
||||
? `${userData.firstName ?? ""} ${userData.lastName ?? ""}`.trim() || userData.email
|
||||
: userData.name || userData.email;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -19,6 +20,11 @@ const DashboardView = async () => {
|
|||
Manager
|
||||
</span>
|
||||
)}
|
||||
{isEmployee(userData) && (
|
||||
<span className="inline-block mt-2 px-3 py-1 bg-green-100 text-green-800 text-sm font-medium rounded-full">
|
||||
Mitarbeiter
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
|
@ -35,6 +41,18 @@ const DashboardView = async () => {
|
|||
{userData.businesses.length === 1 ? "Business" : "Businesses"}
|
||||
</p>
|
||||
</div>
|
||||
) : isEmployee(userData) ? (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-2">Meine Berechtigungen</h2>
|
||||
<ul className="text-gray-600 text-sm space-y-1 mt-2">
|
||||
{userData.permissions.canCreateCourses && <li>• Kurse erstellen</li>}
|
||||
{userData.permissions.canCreateEmployees && <li>• Mitarbeiter anlegen</li>}
|
||||
{!userData.permissions.canCreateCourses &&
|
||||
!userData.permissions.canCreateEmployees && (
|
||||
<li className="text-gray-400">Keine erweiterten Berechtigungen</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-2">Trainings</h2>
|
||||
|
|
@ -135,6 +153,30 @@ const DashboardView = async () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{isEmployee(userData) && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Mitarbeiter-Daten</h2>
|
||||
<dl className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Vorname</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData.firstName || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Nachname</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData.lastName || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">E-Mail</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData.email}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Qualifikation</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData.qualification || "—"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LogoutButton />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"use server";
|
||||
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { getUserData, isManager } from "@/src/lib/auth/getUser";
|
||||
import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser";
|
||||
import { createAdminClient } from "@/src/lib/supabase/admin";
|
||||
import { employeeSchema, type EmployeeFormData } from "../schemas/employee-schema";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { randomBytes } from "crypto";
|
||||
|
|
@ -50,11 +51,13 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
|
|||
};
|
||||
}
|
||||
|
||||
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist (nur Manager dürfen Mitarbeiter erstellen)
|
||||
if (!isManager(userData)) {
|
||||
// Schritt 3: Manager oder Employee mit Berechtigung
|
||||
const canManageEmployees =
|
||||
isManager(userData) || (isEmployee(userData) && userData.permissions.canCreateEmployees);
|
||||
if (!canManageEmployees) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Nur Manager können Mitarbeiter erstellen",
|
||||
error: "Keine Berechtigung zum Erstellen von Mitarbeitern",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +97,14 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
|
|||
onboardingTokenExpiry.setDate(onboardingTokenExpiry.getDate() + 7);
|
||||
}
|
||||
|
||||
// Schritt 7: Mitarbeiter in der Datenbank erstellen
|
||||
// Schritt 7: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz)
|
||||
let managerId = userData.id;
|
||||
if (isEmployee(userData)) {
|
||||
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
|
||||
managerId = selfRecord?.createdBy ?? userData.id;
|
||||
}
|
||||
|
||||
// Schritt 8: Mitarbeiter in der Datenbank erstellen
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
email: validatedData.email,
|
||||
|
|
@ -105,7 +115,7 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
|
|||
isOnboarded: false,
|
||||
onboardingToken,
|
||||
onboardingTokenExpiry,
|
||||
createdBy: userData.id, // Manager-ID als Ersteller
|
||||
createdBy: managerId,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -167,8 +177,11 @@ export async function getMyEmployees() {
|
|||
// Schritt 1: Aktuellen Benutzer abrufen
|
||||
const userData = await getUserData();
|
||||
|
||||
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
|
||||
if (!userData || !isManager(userData)) {
|
||||
// Schritt 2: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
|
||||
if (
|
||||
!userData ||
|
||||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.canCreateEmployees))
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Unauthorized",
|
||||
|
|
@ -177,12 +190,20 @@ export async function getMyEmployees() {
|
|||
}
|
||||
|
||||
// Schritt 3: Mitarbeiter aus der Datenbank abrufen
|
||||
let whereClause: Record<string, unknown>;
|
||||
if (isManager(userData)) {
|
||||
// Manager sieht alle Mitarbeiter unter seiner ID
|
||||
whereClause = { createdBy: userData.id };
|
||||
} else {
|
||||
// Employee: eigene Manager-ID ermitteln, dann alle unter diesem Manager anzeigen (außer sich selbst)
|
||||
const selfRecord = await prisma.employee.findUnique({ where: { email: userData.email } });
|
||||
const managerId = selfRecord?.createdBy;
|
||||
whereClause = { createdBy: managerId, NOT: { email: userData.email } };
|
||||
}
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: {
|
||||
createdBy: userData.id, // Filter nach Manager ID
|
||||
},
|
||||
where: whereClause,
|
||||
orderBy: {
|
||||
createdAt: "desc", // Neueste zuerst
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -214,8 +235,11 @@ export async function getEmployeeById(employeeId: string) {
|
|||
// Schritt 1: Aktuellen Benutzer abrufen
|
||||
const userData = await getUserData();
|
||||
|
||||
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
|
||||
if (!userData || !isManager(userData)) {
|
||||
// Schritt 2: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
|
||||
if (
|
||||
!userData ||
|
||||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.canCreateEmployees))
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Nur Manager können Mitarbeiter bearbeiten",
|
||||
|
|
@ -235,8 +259,16 @@ export async function getEmployeeById(employeeId: string) {
|
|||
};
|
||||
}
|
||||
|
||||
// Schritt 4b: Employee darf eigenen Account nicht bearbeiten
|
||||
if (isEmployee(userData) && employee.email === userData.email) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Sie können Ihren eigenen Account nicht bearbeiten",
|
||||
};
|
||||
}
|
||||
|
||||
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
|
||||
if (employee.createdBy !== userData.id) {
|
||||
if (isManager(userData) && employee.createdBy !== userData.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Sie können nur Ihre eigenen Mitarbeiter bearbeiten",
|
||||
|
|
@ -285,8 +317,10 @@ export async function updateEmployee(
|
|||
};
|
||||
}
|
||||
|
||||
// Schritt 3: Überprüfen, ob Benutzer ein Manager ist
|
||||
if (!isManager(userData)) {
|
||||
// Schritt 3: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
|
||||
const canManageEmployees =
|
||||
isManager(userData) || (isEmployee(userData) && userData.permissions.canCreateEmployees);
|
||||
if (!canManageEmployees) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Nur Manager können Mitarbeiter bearbeiten",
|
||||
|
|
@ -306,8 +340,16 @@ export async function updateEmployee(
|
|||
};
|
||||
}
|
||||
|
||||
// Schritt 5b: Employee darf eigenen Account nicht bearbeiten
|
||||
if (isEmployee(userData) && existingEmployee.email === userData.email) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Sie können Ihren eigenen Account nicht bearbeiten",
|
||||
};
|
||||
}
|
||||
|
||||
// Schritt 6: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
|
||||
if (existingEmployee.createdBy !== userData.id) {
|
||||
if (isManager(userData) && existingEmployee.createdBy !== userData.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Sie können nur Ihre eigenen Mitarbeiter bearbeiten",
|
||||
|
|
@ -471,8 +513,11 @@ export async function deleteEmployee(employeeId: string) {
|
|||
// Schritt 1: Aktuellen Benutzer abrufen
|
||||
const userData = await getUserData();
|
||||
|
||||
// Schritt 2: Überprüfen, ob Benutzer ein Manager ist
|
||||
if (!userData || !isManager(userData)) {
|
||||
// Schritt 2: Überprüfen, ob Benutzer ein Manager oder Employee mit Berechtigung ist
|
||||
if (
|
||||
!userData ||
|
||||
(!isManager(userData) && !(isEmployee(userData) && userData.permissions.canCreateEmployees))
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Nur Manager können Mitarbeiter löschen",
|
||||
|
|
@ -492,8 +537,16 @@ export async function deleteEmployee(employeeId: string) {
|
|||
};
|
||||
}
|
||||
|
||||
// Schritt 4b: Employee darf eigenen Account nicht löschen
|
||||
if (isEmployee(userData) && employee.email === userData.email) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Sie können Ihren eigenen Account nicht löschen",
|
||||
};
|
||||
}
|
||||
|
||||
// Schritt 5: Überprüfen, ob der Manager der Besitzer des Mitarbeiters ist
|
||||
if (employee.createdBy !== userData.id) {
|
||||
if (isManager(userData) && employee.createdBy !== userData.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Sie können nur Ihre eigenen Mitarbeiter löschen",
|
||||
|
|
@ -505,7 +558,22 @@ export async function deleteEmployee(employeeId: string) {
|
|||
where: { id: employeeId },
|
||||
});
|
||||
|
||||
// Schritt 7: Next.js Cache invalidieren
|
||||
// Schritt 7: Supabase Auth User löschen (erzwingt automatisches Ausloggen)
|
||||
if (employee.isOnboarded) {
|
||||
try {
|
||||
const adminClient = createAdminClient();
|
||||
const { data: authUsers } = await adminClient.auth.admin.listUsers();
|
||||
const authUser = authUsers?.users.find((u) => u.email === employee.email);
|
||||
if (authUser) {
|
||||
await adminClient.auth.admin.deleteUser(authUser.id);
|
||||
}
|
||||
} catch (err) {
|
||||
// Auth-Löschung schlägt fehl wenn SERVICE_ROLE_KEY fehlt — DB ist bereits gelöscht
|
||||
console.error("Supabase Auth User konnte nicht gelöscht werden:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 8: Next.js Cache invalidieren
|
||||
revalidatePath("/employee");
|
||||
|
||||
return {
|
||||
|
|
@ -520,3 +588,106 @@ export async function deleteEmployee(employeeId: string) {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft einen Mitarbeiter anhand des Onboarding-Tokens ab (öffentlich, kein Login nötig)
|
||||
*/
|
||||
export async function getEmployeeByOnboardingToken(token: string) {
|
||||
try {
|
||||
const employee = await prisma.employee.findUnique({
|
||||
where: { onboardingToken: token },
|
||||
});
|
||||
|
||||
if (!employee) {
|
||||
return { success: false, error: "Ungültiger Onboarding-Link" };
|
||||
}
|
||||
|
||||
if (employee.isOnboarded) {
|
||||
return { success: false, error: "Onboarding wurde bereits abgeschlossen" };
|
||||
}
|
||||
|
||||
if (employee.onboardingTokenExpiry && employee.onboardingTokenExpiry < new Date()) {
|
||||
return { success: false, error: "Dieser Onboarding-Link ist abgelaufen" };
|
||||
}
|
||||
|
||||
return { success: true, employee };
|
||||
} catch (error) {
|
||||
console.error("Error fetching employee by token:", error);
|
||||
return { success: false, error: "Fehler beim Laden der Mitarbeiterdaten" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt das Onboarding eines Mitarbeiters ab und erstellt einen Supabase-Account
|
||||
*/
|
||||
export async function completeEmployeeOnboarding(
|
||||
token: string,
|
||||
data: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
tel: string;
|
||||
password: string;
|
||||
gender: string;
|
||||
qualification: string;
|
||||
pbSrc?: string;
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const employee = await prisma.employee.findUnique({
|
||||
where: { onboardingToken: token },
|
||||
});
|
||||
|
||||
if (!employee) {
|
||||
return { success: false, error: "Ungültiger Onboarding-Link" };
|
||||
}
|
||||
|
||||
if (employee.isOnboarded) {
|
||||
return { success: false, error: "Onboarding wurde bereits abgeschlossen" };
|
||||
}
|
||||
|
||||
if (employee.onboardingTokenExpiry && employee.onboardingTokenExpiry < new Date()) {
|
||||
return { success: false, error: "Dieser Onboarding-Link ist abgelaufen" };
|
||||
}
|
||||
|
||||
// Supabase Account über API Route erstellen
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||
const res = await fetch(`${baseUrl}/api/auth/register/employee`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email: employee.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
tel: data.tel,
|
||||
gender: data.gender,
|
||||
qualification: data.qualification,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
return { success: false, error: result.error || "Registrierung fehlgeschlagen" };
|
||||
}
|
||||
|
||||
// Mitarbeiter in DB als onboarded markieren
|
||||
await prisma.employee.update({
|
||||
where: { onboardingToken: token },
|
||||
data: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
qualification: data.qualification,
|
||||
pbSrc: data.pbSrc ?? null,
|
||||
isOnboarded: true,
|
||||
onboardingToken: null,
|
||||
onboardingTokenExpiry: null,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error completing onboarding:", error);
|
||||
return { success: false, error: "Ein Fehler ist aufgetreten" };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ export const employeeSchema = z
|
|||
|
||||
// Administrationsrechte
|
||||
permissions: z.object({
|
||||
// Berechtigung zum Anlegen von Kursen
|
||||
// Berechtigung zum Anzeigen/Verwalten von Kursen
|
||||
canCreateCourses: z.boolean(),
|
||||
// Berechtigung zum Anlegen von Mitarbeitern
|
||||
// Berechtigung zum Anzeigen/Verwalten von Mitarbeitern
|
||||
canCreateEmployees: z.boolean(),
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue