From cd04309e3e7841557494dc7ff0e067704110bc4c Mon Sep 17 00:00:00 2001 From: DanielS Date: Wed, 24 Jun 2026 14:26:25 +0200 Subject: [PATCH] feat: add user role management (admin/partner/gesperrt) with login block --- shop/components/admin/user-list.tsx | 92 +++++++++++++++++++---------- shop/components/login-form.tsx | 10 +++- shop/lib/actions/auth.ts | 3 + shop/lib/actions/users.ts | 9 +++ shop/lib/supabase/proxy.ts | 22 +++++++ 5 files changed, 103 insertions(+), 33 deletions(-) diff --git a/shop/components/admin/user-list.tsx b/shop/components/admin/user-list.tsx index 4baf634..6499d9d 100644 --- a/shop/components/admin/user-list.tsx +++ b/shop/components/admin/user-list.tsx @@ -11,24 +11,48 @@ import { TableRow, } from '@/components/ui/table' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { UserX, ShieldAlert, Key, Trash2, Mail } from 'lucide-react' +import { Key, Trash2, Mail, Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' -import { deleteUser, updateUserStatus, resetPassword } from '@/lib/actions/users' +import { deleteUser, resetPassword, updateUserRole } from '@/lib/actions/users' import { Badge } from '@/components/ui/badge' +type Role = 'admin' | 'partner' | 'gesperrt' + +const ROLE_LABELS: Record = { + admin: 'Admin', + partner: 'Partner', + gesperrt: 'Gesperrt', +} + +const ROLE_BADGE_CLASSES: Record = { + admin: 'bg-purple-500/20 text-purple-400 border border-purple-500/30', + partner: 'bg-blue-500/20 text-blue-400 border border-blue-500/30', + gesperrt: 'bg-red-500/20 text-red-400 border border-red-500/30', +} + export function UserList({ initialUsers }: { initialUsers: any[] }) { const router = useRouter() + const [loadingId, setLoadingId] = useState(null) const handleDelete = async (id: string) => { if (confirm('Möchten Sie diesen Benutzer wirklich löschen?')) { + setLoadingId(id) await deleteUser(id) + setLoadingId(null) router.refresh() } } - const handleToggleBan = async (id: string, isBanned: boolean) => { - await updateUserStatus(id, !isBanned) - router.refresh() + const handleRoleChange = async (id: string, newRole: Role) => { + setLoadingId(id + '-role') + try { + await updateUserRole(id, newRole) + router.refresh() + } catch (e) { + console.error('Fehler beim Ändern der Rolle:', e) + } finally { + setLoadingId(null) + } } const handleResetPassword = async (email: string) => { @@ -45,7 +69,7 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { Benutzerverwaltung - Hier können Sie Benutzer neu anlegen, sperren oder Passwörter zurücksetzen. + Verwalten Sie Rollen, Passwörter und Benutzerkonten. @@ -54,14 +78,16 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { E-Mail Rolle - Status Zuletzt angemeldet Aktionen {initialUsers.map((user) => { - const isBanned = !!user.banned_until + const role: Role = (['admin', 'partner', 'gesperrt'].includes(user.role) ? user.role : 'partner') as Role + const isLoadingRole = loadingId === user.id + '-role' + const isLoadingDelete = loadingId === user.id + return ( @@ -71,46 +97,48 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) { - - {user.role === 'admin' ? 'Admin' : 'Partner'} - - - - - {isBanned ? "Gesperrt" : "Aktiv"} - +
+ + {ROLE_LABELS[role]} + + {isLoadingRole ? ( + + ) : ( + + )} +
{user.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString('de-DE') : 'Noch nie'}
- - -
diff --git a/shop/components/login-form.tsx b/shop/components/login-form.tsx index 9b96535..6c45c33 100644 --- a/shop/components/login-form.tsx +++ b/shop/components/login-form.tsx @@ -25,10 +25,13 @@ export function LoginForm({ const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [messageParam, setMessageParam] = useState(null); + const [errorParam, setErrorParam] = useState(null); const router = useRouter(); useEffect(() => { - setMessageParam(new URLSearchParams(window.location.search).get('message')); + const params = new URLSearchParams(window.location.search); + setMessageParam(params.get('message')); + setErrorParam(params.get('error')); }, []); const handleLogin = async (e: React.FormEvent) => { @@ -99,6 +102,11 @@ export function LoginForm({ onChange={(e) => setPassword(e.target.value)} /> + {errorParam === "gesperrt" && ( +

+ 🚫 Ihr Konto wurde gesperrt. Bitte wenden Sie sich an den Administrator. +

+ )} {messageParam === "concurrent" && (

Sie wurden abgemeldet, da Sie sich an einem anderen Gerät angemeldet haben. diff --git a/shop/lib/actions/auth.ts b/shop/lib/actions/auth.ts index 23e7856..23d255a 100644 --- a/shop/lib/actions/auth.ts +++ b/shop/lib/actions/auth.ts @@ -49,6 +49,9 @@ export async function signIn(email: string, password: string) { if (userError || !userData || (userData.role !== 'partner' && userData.role !== 'admin')) { 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." } } diff --git a/shop/lib/actions/users.ts b/shop/lib/actions/users.ts index 55e51c1..475da0c 100644 --- a/shop/lib/actions/users.ts +++ b/shop/lib/actions/users.ts @@ -54,6 +54,15 @@ export async function deleteUser(id: string) { revalidatePath('/admin/users') } +export async function updateUserRole(id: string, role: 'admin' | 'partner' | 'gesperrt') { + const admin = createAdminClient() + const { error } = await admin + .from('users') + .upsert({ id, role }, { onConflict: 'id' }) + if (error) throw error + revalidatePath('/admin/users') +} + export async function updateUserStatus(id: string, ban: boolean) { const admin = createAdminClient() const { error } = await admin.auth.admin.updateUserById(id, { diff --git a/shop/lib/supabase/proxy.ts b/shop/lib/supabase/proxy.ts index f5ae81e..9d74ad2 100644 --- a/shop/lib/supabase/proxy.ts +++ b/shop/lib/supabase/proxy.ts @@ -84,6 +84,28 @@ export async function updateSession(request: NextRequest) { } } + // Gesperrte Benutzer sofort ausloggen + if ( + user?.sub && + !request.nextUrl.pathname.startsWith("/auth") + ) { + const { data: dbUser } = await supabase + .from("users") + .select("role") + .eq("id", user.sub) + .single(); + + if (dbUser?.role === "gesperrt") { + const url = request.nextUrl.clone(); + url.pathname = "/auth/login"; + url.searchParams.set("error", "gesperrt"); + // Clear cookie so the session is fully gone + const response = NextResponse.redirect(url); + response.cookies.delete("webshop-auth-token"); + return response; + } + } + // IMPORTANT: You *must* return the supabaseResponse object as it is. // If you're creating a new response object with NextResponse.next() make sure to: // 1. Pass the request in it, like so: