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
All checks were successful
Staging Build / build (push) Successful in 2m15s
This commit is contained in:
@@ -2,6 +2,7 @@ export const dynamic = 'force-dynamic';
|
||||
import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { getEndCustomers } from '@/lib/actions/end-customers'
|
||||
import type { EndCustomer } from '@/lib/types'
|
||||
import { ArrowLeft, Building2, Plus, Edit2, AlertTriangle } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
@@ -13,10 +14,20 @@ export default async function MyCustomersPage() {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
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
|
||||
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> = {}
|
||||
;(orderCounts ?? []).forEach(o => {
|
||||
@@ -53,6 +64,12 @@ export default async function MyCustomersPage() {
|
||||
</Link>
|
||||
</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 */}
|
||||
{customers.length === 0 && (
|
||||
<Card className="glass-dark border-white/10">
|
||||
|
||||
@@ -21,6 +21,11 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
|
||||
const [userRole, setUserRole] = useState<string>(role);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = 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)
|
||||
const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
@@ -186,7 +191,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</nav>
|
||||
@@ -269,7 +274,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
|
||||
</Button>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
@@ -24,8 +24,13 @@ export function LoginForm({
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [messageParam, setMessageParam] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setMessageParam(new URLSearchParams(window.location.search).get('message'));
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
@@ -35,6 +40,10 @@ export function LoginForm({
|
||||
|
||||
try {
|
||||
const res = await signIn(email, password);
|
||||
if (!res.success) {
|
||||
setError(res.error || "An error occurred");
|
||||
return;
|
||||
}
|
||||
if (nextParam) {
|
||||
router.push(nextParam);
|
||||
} else if (res.role === "admin") {
|
||||
@@ -90,17 +99,11 @@ export function LoginForm({
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{(() => {
|
||||
const messageParam = typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('message') : null;
|
||||
if (messageParam === "concurrent") {
|
||||
return (
|
||||
<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;
|
||||
})()}
|
||||
{messageParam === "concurrent" && (
|
||||
<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>
|
||||
)}
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Logging in..." : "Login"}
|
||||
|
||||
@@ -1,38 +1,68 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { createAdminClient } from '@/lib/supabase/admin'
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
export async function signIn(email: string, password: string) {
|
||||
const supabase = await createClient()
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (error) {
|
||||
throw new 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.")
|
||||
try {
|
||||
const supabase = await createClient()
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
// Andere aktive Sitzungen beenden
|
||||
await supabase.auth.signOut({ scope: 'others' })
|
||||
const userId = data.user?.id
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user