feat(branding): add global dynamic theme system and fix 2fa mail
This commit is contained in:
@@ -15,47 +15,6 @@ export async function signIn(email: string, password: string, deviceHash?: strin
|
||||
password,
|
||||
})
|
||||
if (error) {
|
||||
// Track failed attempts
|
||||
const { data: usr, error: usrError } = await supabase
|
||||
.from('users')
|
||||
.select('failed_attempts, email')
|
||||
.eq('email', email)
|
||||
.single()
|
||||
|
||||
const attempts = (usr?.failed_attempts ?? 0) + 1
|
||||
|
||||
// Update attempts count
|
||||
await supabase
|
||||
.from('users')
|
||||
.update({ failed_attempts: attempts })
|
||||
.eq('email', email)
|
||||
|
||||
// Lock account on 5th failure
|
||||
if (attempts >= 5) {
|
||||
await supabase
|
||||
.from('users')
|
||||
.update({ role: 'gesperrt' })
|
||||
.eq('email', email)
|
||||
// Notify all admins
|
||||
try {
|
||||
const adminClient = createAdminClient()
|
||||
const { data: admins } = await adminClient
|
||||
.from('users')
|
||||
.select('email')
|
||||
.eq('role', 'admin')
|
||||
|
||||
if (admins && admins.length > 0) {
|
||||
const adminEmails = admins.map(a => a.email).filter(Boolean)
|
||||
for (const adminEmail of adminEmails) {
|
||||
await sendLockoutEmail(adminEmail)
|
||||
}
|
||||
}
|
||||
} catch (mailError) {
|
||||
console.error('Failed to notify admins of lockout:', mailError)
|
||||
}
|
||||
return { success: false, error: 'Konto gesperrt nach 5 Fehlversuchen.' }
|
||||
}
|
||||
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
@@ -243,15 +202,12 @@ async function send2FACodeInternal(userId: string, deviceHash: string, email: st
|
||||
export async function resend2FACode(userId: string, deviceHash: string) {
|
||||
try {
|
||||
const adminClient = createAdminClient()
|
||||
const { data: user } = await adminClient
|
||||
.from('users')
|
||||
.select('email')
|
||||
.eq('id', userId)
|
||||
.single()
|
||||
const { data: authUser, error: authError } = await adminClient.auth.admin.getUserById(userId)
|
||||
const email = authUser?.user?.email
|
||||
|
||||
if (!user?.email) return { success: false, error: 'Benutzer nicht gefunden.' }
|
||||
if (authError || !email) return { success: false, error: 'Benutzer-E-Mail nicht gefunden.' }
|
||||
|
||||
await send2FACodeInternal(userId, deviceHash, user.email)
|
||||
await send2FACodeInternal(userId, deviceHash, email)
|
||||
return { success: true }
|
||||
} catch (err: any) {
|
||||
console.error('Resend 2FA error:', err)
|
||||
@@ -263,17 +219,36 @@ export async function resend2FACode(userId: string, deviceHash: string) {
|
||||
export async function verifyDevice2FA(userId: string, code: string, deviceHash: string) {
|
||||
try {
|
||||
const adminClient = createAdminClient()
|
||||
const cleanCode = code.trim()
|
||||
|
||||
// Code prüfen
|
||||
const { data: codeEntry, error: codeError } = await adminClient
|
||||
// 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('device_hash', deviceHash)
|
||||
.eq('code', code)
|
||||
.maybeSingle()
|
||||
.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.' }
|
||||
}
|
||||
|
||||
@@ -291,22 +266,23 @@ export async function verifyDevice2FA(userId: string, code: string, deviceHash:
|
||||
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: deviceHash,
|
||||
device_hash: targetDeviceHash,
|
||||
user_agent: userAgent,
|
||||
ip_address: ip,
|
||||
verified_at: new Date().toISOString(),
|
||||
}, { onConflict: 'user_id,device_hash' })
|
||||
|
||||
// Verbrauchten Code löschen
|
||||
// Verbrauchte Codes für diesen User löschen
|
||||
await adminClient
|
||||
.from('device_verification_codes')
|
||||
.delete()
|
||||
.eq('id', codeEntry.id)
|
||||
.eq('user_id', userId)
|
||||
|
||||
// Andere Sessions beenden
|
||||
const supabase = await createClient()
|
||||
@@ -322,7 +298,7 @@ export async function verifyDevice2FA(userId: string, code: string, deviceHash:
|
||||
return { success: true, role: userData?.role || 'partner' }
|
||||
} catch (err: any) {
|
||||
console.error('Verify 2FA error:', err)
|
||||
return { success: false, error: 'Fehler bei der Verifizierung.' }
|
||||
return { success: false, error: 'Fehler bei der Code-Überprüfung.' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user