fix: production login error masking, resolve navbar hydration mismatch, and gracefully handle database render errors
All checks were successful
Staging Build / build (push) Successful in 2m15s

This commit is contained in:
DanielS
2026-06-24 10:54:00 +02:00
parent e1f47ddcd0
commit 5c8c198fc5
4 changed files with 96 additions and 41 deletions

View File

@@ -2,6 +2,7 @@ export const dynamic = 'force-dynamic';
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import { getEndCustomers } from '@/lib/actions/end-customers' import { getEndCustomers } from '@/lib/actions/end-customers'
import type { EndCustomer } from '@/lib/types'
import { ArrowLeft, Building2, Plus, Edit2, AlertTriangle } from 'lucide-react' import { ArrowLeft, Building2, Plus, Edit2, AlertTriangle } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
@@ -13,10 +14,20 @@ export default async function MyCustomersPage() {
const { data: { user } } = await supabase.auth.getUser() const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/auth/login') if (!user) redirect('/auth/login')
const customers = await getEndCustomers() let customers: EndCustomer[] = []
let fetchError: string | null = null
try {
customers = await getEndCustomers()
} catch (err: any) {
console.error("Error loading end customers:", err)
fetchError = err.message || "Es gab ein Problem beim Laden Ihrer Endkunden."
}
// Anzahl Bestellungen pro Endkunde // Anzahl Bestellungen pro Endkunde
const { data: orderCounts } = await supabase.from('orders').select('end_customer_id').not('end_customer_id', 'is', null); const { data: orderCounts, error: orderError } = await supabase.from('orders').select('end_customer_id').not('end_customer_id', 'is', null);
if (orderError) {
console.error("Error loading order counts:", orderError)
}
const countMap: Record<string, number> = {} const countMap: Record<string, number> = {}
;(orderCounts ?? []).forEach(o => { ;(orderCounts ?? []).forEach(o => {
@@ -53,6 +64,12 @@ export default async function MyCustomersPage() {
</Link> </Link>
</div> </div>
{fetchError && (
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-200 text-sm">
{fetchError}
</div>
)}
{/* Keine Kunden */} {/* Keine Kunden */}
{customers.length === 0 && ( {customers.length === 0 && (
<Card className="glass-dark border-white/10"> <Card className="glass-dark border-white/10">

View File

@@ -21,6 +21,11 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
const [userRole, setUserRole] = useState<string>(role); const [userRole, setUserRole] = useState<string>(role);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [pathname, setPathname] = useState("/");
useEffect(() => {
setPathname(window.location.pathname);
}, []);
// Supabase URL & Anon Key auslesen (für Browser-Client) // Supabase URL & Anon Key auslesen (für Browser-Client)
const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
@@ -186,7 +191,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
</div> </div>
) : ( ) : (
<Button asChild className="bg-primary hover:bg-primary/95 text-white dark:text-blue-500"> <Button asChild className="bg-primary hover:bg-primary/95 text-white dark:text-blue-500">
<Link href={`/auth/login?next=${typeof window !== 'undefined' ? window.location.pathname : '/'}`}>Anmelden</Link> <Link href={`/auth/login?next=${pathname}`}>Anmelden</Link>
</Button> </Button>
)} )}
</nav> </nav>
@@ -269,7 +274,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
</Button> </Button>
) : ( ) : (
<Button asChild className="w-full bg-primary hover:bg-primary/95 text-white dark:text-blue-500" onClick={() => setIsMobileMenuOpen(false)}> <Button asChild className="w-full bg-primary hover:bg-primary/95 text-white dark:text-blue-500" onClick={() => setIsMobileMenuOpen(false)}>
<Link href={`/auth/login?next=${typeof window !== 'undefined' ? window.location.pathname : '/'}`}>Anmelden</Link> <Link href={`/auth/login?next=${pathname}`}>Anmelden</Link>
</Button> </Button>
)} )}
</div> </div>

View File

@@ -14,7 +14,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState, useEffect } from "react";
export function LoginForm({ export function LoginForm({
className, className,
@@ -24,8 +24,13 @@ export function LoginForm({
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [messageParam, setMessageParam] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
useEffect(() => {
setMessageParam(new URLSearchParams(window.location.search).get('message'));
}, []);
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
@@ -35,6 +40,10 @@ export function LoginForm({
try { try {
const res = await signIn(email, password); const res = await signIn(email, password);
if (!res.success) {
setError(res.error || "An error occurred");
return;
}
if (nextParam) { if (nextParam) {
router.push(nextParam); router.push(nextParam);
} else if (res.role === "admin") { } else if (res.role === "admin") {
@@ -90,17 +99,11 @@ export function LoginForm({
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
</div> </div>
{(() => { {messageParam === "concurrent" && (
const messageParam = typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('message') : null; <p className="text-sm text-amber-500 bg-amber-500/10 border border-amber-500/20 p-3 rounded-lg text-center font-medium">
if (messageParam === "concurrent") { Sie wurden abgemeldet, da Sie sich an einem anderen Gerät angemeldet haben.
return ( </p>
<p className="text-sm text-amber-500 bg-amber-500/10 border border-amber-500/20 p-3 rounded-lg text-center font-medium"> )}
Sie wurden abgemeldet, da Sie sich an einem anderen Gerät angemeldet haben.
</p>
);
}
return null;
})()}
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}> <Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Logging in..." : "Login"} {isLoading ? "Logging in..." : "Login"}

View File

@@ -1,38 +1,68 @@
'use server' 'use server'
import { createClient } from '@/lib/supabase/server' import { createClient } from '@/lib/supabase/server'
import { createAdminClient } from '@/lib/supabase/admin'
import { headers } from 'next/headers' import { headers } from 'next/headers'
export async function signIn(email: string, password: string) { export async function signIn(email: string, password: string) {
const supabase = await createClient() try {
const { data, error } = await supabase.auth.signInWithPassword({ const supabase = await createClient()
email, const { data, error } = await supabase.auth.signInWithPassword({
password, email,
}) password,
if (error) { })
throw new Error(error.message) if (error) {
} return { success: false, error: error.message }
const userId = data.user?.id
if (userId) {
const { data: userData, error: userError } = await supabase
.from('users')
.select('role')
.eq('id', userId)
.single()
if (userError || !userData || (userData.role !== 'partner' && userData.role !== 'admin')) {
await supabase.auth.signOut()
throw new Error("Zugriff verweigert. Nur registrierte Partner dürfen sich anmelden.")
} }
// Andere aktive Sitzungen beenden const userId = data.user?.id
await supabase.auth.signOut({ scope: 'others' }) if (userId) {
let { data: userData, error: userError } = await supabase
.from('users')
.select('role')
.eq('id', userId)
.single()
return { success: true, role: userData.role } 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')) {
await supabase.auth.signOut()
return { success: false, error: "Zugriff verweigert. Nur registrierte Partner dürfen sich anmelden." }
}
// 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." }
} }
return { success: true, role: 'partner' }
} }
export async function signUp(email: string, password: string) { export async function signUp(email: string, password: string) {