'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 await adminClient .from('device_verification_codes') .insert({ user_id: userId, code, device_hash: deviceHash, expires_at: expiresAt }) // 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) 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 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 }