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 { 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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user