'use server' import { createClient } from '@/lib/supabase/server' import { createAdminClient } from '@/lib/supabase/admin' import { headers } from 'next/headers' import { sendLockoutEmail } from '@/lib/utils/email' import { sendMail } from '@/utils/mail' import crypto from 'crypto' export async function signIn(email: string, password: string, deviceHash?: string) { try { const supabase = await createClient() const { data, error } = await supabase.auth.signInWithPassword({ email, password, }) if (error) { return { success: false, error: error.message } } const userId = data.user?.id if (userId) { const adminClient = createAdminClient() let { data: userData, error: userError } = await adminClient .from('users') .select('role') .eq('id', userId) .single() if (userError || !userData) { // Fallback: create database records if missing for this authenticated user try { const adminSupabase = createAdminClient() // Ensure profile exists await adminSupabase.from('profiles').insert([{ id: userId }]).select() // Ensure user exists const { data: insertedUser, error: insertError } = await adminSupabase .from('users') .insert([{ id: userId, role: 'partner' }]) .select('role') .single() if (!insertError && insertedUser) { userData = insertedUser userError = null } } catch (adminErr) { console.error("Failed to backfill user records:", adminErr) } } if (userError || !userData || (userData.role !== 'partner' && userData.role !== 'admin' && userData.role !== 'verwaltung')) { await supabase.auth.signOut() if (userData?.role === 'gesperrt') { return { success: false, error: "Ihr Konto wurde gesperrt. Bitte wenden Sie sich an den Administrator." } } return { success: false, error: "Zugriff verweigert. Nur registrierte Partner dürfen sich anmelden." } } // --- 2FA: Geräteprüfung --- if (deviceHash) { const { data: knownDevice } = await adminClient .from('known_devices') .select('id') .eq('user_id', userId) .eq('device_hash', deviceHash) .maybeSingle() if (!knownDevice) { // Gerät unbekannt -> 2FA erforderlich // Session bleibt aktiv, aber Client muss 2FA bestätigen await send2FACodeInternal(userId, deviceHash, email) return { success: true, requires2FA: true, userId, role: userData.role } } } // Andere aktive Sitzungen beenden await supabase.auth.signOut({ scope: 'others' }) return { success: true, role: userData.role } } return { success: true, role: 'partner' } } catch (err: any) { console.error("SignIn error:", err) return { success: false, error: err?.message || "Ein unerwarteter Fehler ist aufgetreten." } } } export async function signUp(email: string, password: string) { throw new Error("Registrierung ist deaktiviert.") } export async function signOut() { const supabase = await createClient() const { error } = await supabase.auth.signOut() if (error) { throw new Error(error.message) } return { success: true } } function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) { return `

${title}

${messageHtml} ${buttonText && buttonUrl ? `
${buttonText}

Falls der Button nicht funktioniert, kopieren Sie bitte diesen Link in Ihren Browser:
${buttonUrl}

` : ''}

Dies ist eine automatisch generierte E-Mail.
© ${new Date().getFullYear()} CASPOS Shop. Alle Rechte vorbehalten.

`; } function get2FAEmailHtml(code: string) { const digits = code.split('') const digitBoxes = digits.map(d => `${d}` ).join('') const messageHtml = `

Hallo,

Wir haben eine Anmeldung von einem neuen Gerät erkannt. Bitte bestätigen Sie Ihre Identität mit folgendem Sicherheitscode:

${digitBoxes}

Dieser Code ist 15 Minuten gültig.
Falls Sie diese Anmeldung nicht durchgeführt haben, ändern Sie bitte umgehend Ihr Passwort.

Dieser Code dient der Sicherheit Ihres Kontos. Geben Sie ihn niemals an andere Personen weiter.

` return getBeautifulEmailHtml('Sicherheitscode', messageHtml) } /** Internal: generates code + sends email (no auth check needed) */ async function send2FACodeInternal(userId: string, deviceHash: string, email: string) { const adminClient = createAdminClient() const code = Math.floor(100000 + Math.random() * 900000).toString() const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString() // Alte Codes für dieses Gerät löschen await adminClient .from('device_verification_codes') .delete() .eq('user_id', userId) .eq('device_hash', deviceHash) // Neuen Code speichern const { data: insData, error: insError } = await adminClient .from('device_verification_codes') .insert({ user_id: userId, code, device_hash: deviceHash, expires_at: expiresAt }) .select() if (insError) { console.error("2FA CODE INSERT ERROR:", insError) } else { console.log("2FA CODE INSERTED:", code, insData) } // Mail senden await sendMail({ to: email, subject: 'Ihr Sicherheitscode – CASPOS Shop', text: `Ihr Sicherheitscode lautet: ${code}\n\nDieser Code ist 15 Minuten gültig.\n\nFalls Sie diese Anmeldung nicht durchgeführt haben, ändern Sie bitte umgehend Ihr Passwort.`, html: get2FAEmailHtml(code), }) } /** Public server action: resend 2FA code */ export async function resend2FACode(userId: string, deviceHash: string) { try { const adminClient = createAdminClient() const { data: authUser, error: authError } = await adminClient.auth.admin.getUserById(userId) const email = authUser?.user?.email if (authError || !email) return { success: false, error: 'Benutzer-E-Mail nicht gefunden.' } await send2FACodeInternal(userId, deviceHash, email) return { success: true } } catch (err: any) { console.error('Resend 2FA error:', err) return { success: false, error: 'Fehler beim erneuten Senden des Codes.' } } } /** Verify 2FA code and register device */ export async function verifyDevice2FA(userId: string, code: string, deviceHash: string) { try { const adminClient = createAdminClient() const cleanCode = code.trim() // Code prüfen (unter Berücksichtigung von Leerzeichen & device_hash) console.log("VERIFYING 2FA: userId =", userId, "code =", cleanCode) const { data: codeEntries, error: codeError } = await adminClient .from('device_verification_codes') .select('*') .eq('user_id', userId) .eq('code', cleanCode) .order('created_at', { ascending: false }) .limit(1) const codeEntry = codeEntries && codeEntries.length > 0 ? codeEntries[0] : null console.log("VERIFY 2FA DB RESULT: entry =", codeEntry, "error =", codeError) if (codeError || !codeEntry) { // Doppelklick- & Race-Condition Schutz: Prüfen ob das Gerät eben bereits verifiziert wurde const { data: knownDev } = await adminClient .from('known_devices') .select('*') .eq('user_id', userId) .limit(1) if (knownDev && knownDev.length > 0) { const { data: userData } = await adminClient .from('users') .select('role') .eq('id', userId) .single() return { success: true, role: userData?.role || 'partner' } } return { success: false, error: 'Ungültiger Sicherheitscode.' } } // Ablauf prüfen if (new Date(codeEntry.expires_at) < new Date()) { // Code abgelaufen -> löschen await adminClient .from('device_verification_codes') .delete() .eq('id', codeEntry.id) return { success: false, error: 'Der Code ist abgelaufen. Bitte fordern Sie einen neuen Code an.' } } // Gerät registrieren const headersList = await headers() const userAgent = headersList.get('user-agent') || '' const ip = headersList.get('x-forwarded-for')?.split(',')[0]?.trim() || headersList.get('x-real-ip') || '' const targetDeviceHash = deviceHash || codeEntry.device_hash || 'default-device' await adminClient .from('known_devices') .upsert({ user_id: userId, device_hash: targetDeviceHash, user_agent: userAgent, ip_address: ip, verified_at: new Date().toISOString(), }, { onConflict: 'user_id,device_hash' }) // Verbrauchte Codes für diesen User löschen await adminClient .from('device_verification_codes') .delete() .eq('user_id', userId) // Andere Sessions beenden const supabase = await createClient() await supabase.auth.signOut({ scope: 'others' }) // Rolle zurückgeben const { data: userData } = await adminClient .from('users') .select('role') .eq('id', userId) .single() return { success: true, role: userData?.role || 'partner' } } catch (err: any) { console.error('Verify 2FA error:', err) return { success: false, error: 'Fehler bei der Code-Überprüfung.' } } } export async function resetPassword(email: string) { try { const admin = createAdminClient() const { data, error } = await admin.auth.admin.generateLink({ type: 'recovery', email, }) if (error) { return { success: false, error: error.message } } const tokenHash = (data as any)?.properties?.hashed_token; if (!tokenHash) { return { success: false, error: 'Wiederherstellungs-Token konnte nicht generiert werden.' } } const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de' const resetLink = `${siteUrl}/auth/verify-link?token_hash=${tokenHash}&type=recovery&next=/auth/update-password` const messageHtml = `

Hallo,

Sie haben das Zurücksetzen Ihres Passworts angefordert.

Bitte klicken Sie auf den untenstehenden Button, um ein neues Passwort festzulegen:

`; await sendMail({ to: email, subject: 'Passwort zurücksetzen – CASPOS Shop', text: `Hallo,\n\nSie haben das Zurücksetzen Ihres Passworts angefordert.\n\nBitte klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:\n${resetLink}\n\nViele Grüße,\nIhr CASPOS Shop-Team`, html: getBeautifulEmailHtml('Passwort zurücksetzen', messageHtml, 'Passwort zurücksetzen', resetLink) }) return { success: true } } catch (err: any) { console.error('Password reset error:', err) return { success: false, error: err.message || 'Fehler beim Senden der Passwort-Zurücksetzen-E-Mail.' } } } export async function updatePassword(password: string) { try { const supabase = await createClient() const { error } = await supabase.auth.updateUser({ password }) if (error) { return { success: false, error: error.message } } return { success: true } } catch (err: any) { console.error('Update password error:', err) return { success: false, error: err.message || 'Fehler beim Aktualisieren des Passworts.' } } } export async function verifyAdmin() { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) throw new Error("Nicht authentifiziert.") const { data: userData, error } = await supabase .from('users') .select('role') .eq('id', user.id) .single() if (error || !userData || userData.role !== 'admin') { throw new Error("Zugriff verweigert. Nur Administratoren erlaubt.") } return user }