mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:41:16 +00:00
Merge pull request #70 from Since-3/bug/address-issues
addressed a few issues
This commit is contained in:
commit
df09e04d4b
6 changed files with 117 additions and 33 deletions
|
|
@ -1,15 +1,75 @@
|
|||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
|
||||
// Simple in-memory rate limiter: max 5 attempts per IP per 15 minutes
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
||||
const RATE_LIMIT_MAX = 5;
|
||||
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
||||
|
||||
function isRateLimited(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const entry = rateLimitMap.get(ip);
|
||||
|
||||
if (!entry || now > entry.resetAt) {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.count >= RATE_LIMIT_MAX) return true;
|
||||
|
||||
entry.count++;
|
||||
return false;
|
||||
}
|
||||
|
||||
const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{8,}$/;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, password, firstName, lastName, tel, gender, qualification } = body;
|
||||
const ip =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0].trim() ??
|
||||
request.headers.get("x-real-ip") ??
|
||||
"unknown";
|
||||
|
||||
if (!email || !password || !firstName || !lastName) {
|
||||
if (isRateLimited(ip)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Zu viele Anfragen. Bitte warte 15 Minuten." },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { token, email, password, firstName, lastName, tel, gender, qualification } = body;
|
||||
|
||||
if (!token || !email || !password || !firstName || !lastName) {
|
||||
return NextResponse.json({ error: "Pflichtfelder fehlen" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Token serverseitig validieren
|
||||
const employee = await prisma.employee.findUnique({
|
||||
where: { onboardingToken: token },
|
||||
select: { email: true, isOnboarded: true, onboardingTokenExpiry: true },
|
||||
});
|
||||
|
||||
if (!employee || employee.isOnboarded || employee.email !== email) {
|
||||
return NextResponse.json({ error: "Ungültiger Token" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (employee.onboardingTokenExpiry && employee.onboardingTokenExpiry < new Date()) {
|
||||
return NextResponse.json({ error: "Token abgelaufen" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Passwort-Stärke serverseitig prüfen
|
||||
if (!PASSWORD_REGEX.test(password)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"Passwort muss mindestens 8 Zeichen, Groß-/Kleinbuchstaben, eine Zahl und ein Sonderzeichen enthalten",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
|
|
|
|||
|
|
@ -1,20 +1,35 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { createAdminClient } from "@/src/lib/supabase/admin";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Erwartet: multipart/form-data mit "file" und "token" 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;
|
||||
const token = formData.get("token") as string | null;
|
||||
|
||||
if (!file || !path) {
|
||||
return NextResponse.json({ error: "Datei und Pfad erforderlich" }, { status: 400 });
|
||||
if (!file || !token) {
|
||||
return NextResponse.json({ error: "Datei und Token erforderlich" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Token serverseitig validieren
|
||||
const employee = await prisma.employee.findUnique({
|
||||
where: { onboardingToken: token },
|
||||
select: { id: true, isOnboarded: true, onboardingTokenExpiry: true },
|
||||
});
|
||||
|
||||
if (!employee || employee.isOnboarded) {
|
||||
return NextResponse.json({ error: "Ungültiger Token" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (employee.onboardingTokenExpiry && employee.onboardingTokenExpiry < new Date()) {
|
||||
return NextResponse.json({ error: "Token abgelaufen" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
|
|
@ -25,11 +40,19 @@ export async function POST(request: Request) {
|
|||
return NextResponse.json({ error: "Datei darf maximal 5MB groß sein" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Pfad serverseitig generieren — kein Client-Input
|
||||
const extension =
|
||||
file.name
|
||||
.split(".")
|
||||
.pop()
|
||||
?.replace(/[^a-zA-Z0-9]/g, "") || "jpg";
|
||||
const safePath = `employees/${employee.id}/${Date.now()}.${extension}`;
|
||||
|
||||
const adminClient = createAdminClient();
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const { data, error } = await adminClient.storage.from("avatars").upload(path, buffer, {
|
||||
const { data, error } = await adminClient.storage.from("avatars").upload(safePath, buffer, {
|
||||
contentType: file.type,
|
||||
upsert: true,
|
||||
cacheControl: "3600",
|
||||
|
|
|
|||
|
|
@ -153,8 +153,13 @@ export const getUserData = cache(async (): Promise<AuthUserData | null> => {
|
|||
}
|
||||
|
||||
// Try Employee table — match by email since Supabase ID is not stored in Employee
|
||||
if (!user.email) {
|
||||
console.log("⚠️ [DB] User has no email, cannot lookup employee");
|
||||
return null;
|
||||
}
|
||||
|
||||
const employeeData = await prisma.employee.findUnique({
|
||||
where: { email: user.email! },
|
||||
where: { email: user.email },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
getEmployeeByOnboardingToken,
|
||||
completeEmployeeOnboarding,
|
||||
} from "@/src/modules/employee/actions/employee-actions";
|
||||
import { generateFilePath } from "@/src/lib/supabase/storage";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const step1Schema = z
|
||||
|
|
@ -124,10 +123,9 @@ const EmployeeOnboardingView = () => {
|
|||
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);
|
||||
formData.append("token", token);
|
||||
const res = await fetch("/api/upload/avatar", { method: "POST", body: formData });
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json.error);
|
||||
|
|
|
|||
|
|
@ -29,19 +29,13 @@ 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 AvatarImage = ({ userData }: { userData: AuthUserData }) => {
|
||||
const pbSrc = "pbSrc" in userData ? userData.pbSrc : null;
|
||||
const src = pbSrc || getDiceBearUrl(displayName);
|
||||
const src = pbSrc || getDiceBearUrl(userData.id);
|
||||
|
||||
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 />
|
||||
<Image src={src} alt="" fill className="object-cover" unoptimized />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -114,7 +108,7 @@ const DashboardFooter = ({ displayName, userData }: DashboardFooterProps) => {
|
|||
|
||||
{/* DisplayName + Avatar */}
|
||||
<div className="w-full flex items-center gap-4 p-2 mt-2">
|
||||
<AvatarImage userData={userData} displayName={displayName} />
|
||||
<AvatarImage userData={userData} />
|
||||
<p className="truncate text-sm font-medium">{displayName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -642,20 +642,13 @@ export async function completeEmployeeOnboarding(
|
|||
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({
|
||||
token,
|
||||
email: employee.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
|
|
@ -672,9 +665,13 @@ export async function completeEmployeeOnboarding(
|
|||
return { success: false, error: result.error || "Registrierung fehlgeschlagen" };
|
||||
}
|
||||
|
||||
// Mitarbeiter in DB als onboarded markieren
|
||||
await prisma.employee.update({
|
||||
where: { onboardingToken: token },
|
||||
// Mitarbeiter atomisch als onboarded markieren — verhindert TOCTOU race condition
|
||||
const updated = await prisma.employee.updateMany({
|
||||
where: {
|
||||
onboardingToken: token,
|
||||
isOnboarded: false,
|
||||
OR: [{ onboardingTokenExpiry: null }, { onboardingTokenExpiry: { gte: new Date() } }],
|
||||
},
|
||||
data: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
|
|
@ -686,6 +683,13 @@ export async function completeEmployeeOnboarding(
|
|||
},
|
||||
});
|
||||
|
||||
if (updated.count === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Onboarding wurde bereits abgeschlossen oder der Link ist abgelaufen",
|
||||
};
|
||||
}
|
||||
|
||||
// Manager-Bestätigungs-E-Mail senden
|
||||
if (employee.createdBy) {
|
||||
const manager = await prisma.manager.findUnique({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue