From 4f69fd7e44b7ec5bb329f25f493f70215bc2b194 Mon Sep 17 00:00:00 2001 From: DanielS Date: Fri, 3 Jul 2026 10:10:57 +0200 Subject: [PATCH] feat(role): add 'verwaltung' role with gated access to settings and tools --- shop/app/admin/einstellungen/page.tsx | 33 ++++++++++++++++++-- shop/app/admin/layout.tsx | 39 +++++++++++++---------- shop/app/admin/settings/page.tsx | 45 ++++++++++++++++++++------- shop/app/admin/tools/page.tsx | 31 ++++++++++++++++-- shop/components/admin/user-dialog.tsx | 3 +- shop/components/admin/user-list.tsx | 15 ++++++--- shop/lib/actions/users.ts | 4 +-- 7 files changed, 129 insertions(+), 41 deletions(-) diff --git a/shop/app/admin/einstellungen/page.tsx b/shop/app/admin/einstellungen/page.tsx index e0cab5a..a758cc6 100644 --- a/shop/app/admin/einstellungen/page.tsx +++ b/shop/app/admin/einstellungen/page.tsx @@ -5,6 +5,8 @@ import { useState, useEffect } from 'react'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; import { Download, Upload, Database, AlertTriangle, Loader2 } from 'lucide-react'; +import { createClient } from '@/lib/supabase/client'; +import { useRouter } from 'next/navigation'; export default function AdminSettings() { const [demoActive, setDemoActive] = useState(true); @@ -13,11 +15,32 @@ export default function AdminSettings() { const [statusMsg, setStatusMsg] = useState(''); const [statusType, setStatusType] = useState<'success' | 'error' | 'info' | ''>(''); const [selectedFile, setSelectedFile] = useState(null); + const [loading, setLoading] = useState(true); + const router = useRouter(); useEffect(() => { - const state = localStorage.getItem('demo_banner_disabled') !== 'true'; - setDemoActive(state); - }, []); + async function checkAccess() { + const supabase = createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + router.push('/auth/login'); + return; + } + const { data: userData } = await supabase + .from('users') + .select('role') + .eq('id', user.id) + .single(); + if (!userData || userData.role === 'verwaltung') { + router.push('/admin'); + } else { + const state = localStorage.getItem('demo_banner_disabled') !== 'true'; + setDemoActive(state); + setLoading(false); + } + } + checkAccess(); + }, [router]); const toggleDemo = (checked: boolean) => { setDemoActive(checked); @@ -85,6 +108,10 @@ export default function AdminSettings() { } }; + if (loading) { + return
; + } + return (
{/* Page Header */} diff --git a/shop/app/admin/layout.tsx b/shop/app/admin/layout.tsx index af738a4..4c0c367 100644 --- a/shop/app/admin/layout.tsx +++ b/shop/app/admin/layout.tsx @@ -20,10 +20,13 @@ export default async function AdminLayout({ .eq('id', user.id) .single() - if (!userData || userData.role !== 'admin') { + const userRole = userData?.role + if (!userData || (userRole !== 'admin' && userRole !== 'verwaltung')) { redirect('/') } + const isVerwaltung = userRole === 'verwaltung' + return (
{/* Sidebar */} @@ -64,21 +67,25 @@ export default async function AdminLayout({ Firmen -
- Einstellungen -
- - - Allgemein - - - - SMTP - - - - DB-Tools - + {!isVerwaltung && ( + <> +
+ Einstellungen +
+ + + Allgemein + + + + SMTP + + + + DB-Tools + + + )}
diff --git a/shop/app/admin/settings/page.tsx b/shop/app/admin/settings/page.tsx index 71eddee..928de99 100644 --- a/shop/app/admin/settings/page.tsx +++ b/shop/app/admin/settings/page.tsx @@ -1,7 +1,9 @@ "use client"; -// Admin Settings page – view & edit SMTP configuration and send test email + import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; +import { createClient } from '@/lib/supabase/client'; +import { Loader2 } from 'lucide-react'; interface Settings { host: string; @@ -17,17 +19,36 @@ export default function SettingsPage() { const [message, setMessage] = useState(''); const router = useRouter(); - // Load current settings + // Load current settings and verify access useEffect(() => { - fetch('/api/admin/smtp-settings') - .then((res) => res.json()) - .then((data) => { - if (data.settings) setSettings(data.settings as Settings); - else setMessage('Failed to load settings'); - }) - .catch(() => setMessage('Failed to load settings')) - .finally(() => setLoading(false)); - }, []); + async function checkAccessAndLoad() { + const supabase = createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + router.push('/auth/login'); + return; + } + const { data: userData } = await supabase + .from('users') + .select('role') + .eq('id', user.id) + .single(); + if (!userData || userData.role === 'verwaltung') { + router.push('/admin'); + return; + } + + fetch('/api/admin/smtp-settings') + .then((res) => res.json()) + .then((data) => { + if (data.settings) setSettings(data.settings as Settings); + else setMessage('Failed to load settings'); + }) + .catch(() => setMessage('Failed to load settings')) + .finally(() => setLoading(false)); + } + checkAccessAndLoad(); + }, [router]); const handleChange = (e: React.ChangeEvent) => { const { name, value, type, checked } = e.target; @@ -69,7 +90,7 @@ export default function SettingsPage() { else setMessage(data.error || 'Failed to send test email'); }; - if (loading) return
Loading…
; + if (loading) return
; return (
diff --git a/shop/app/admin/tools/page.tsx b/shop/app/admin/tools/page.tsx index 63a3c46..ff9e6a2 100644 --- a/shop/app/admin/tools/page.tsx +++ b/shop/app/admin/tools/page.tsx @@ -11,6 +11,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Badge } from "@/components/ui/badge"; import { Loader2, RefreshCw, Wrench, Database, AlertTriangle, CheckCircle2, XCircle, Terminal, ArrowLeft } from "lucide-react"; import Link from "next/link"; +import { createClient } from "@/lib/supabase/client"; +import { useRouter } from "next/navigation"; interface TableStatus { exists: boolean; @@ -34,6 +36,8 @@ export default function AdminToolsPage() { const [isLoading, setIsLoading] = useState(false); const [logs, setLogs] = useState([]); const [activeAction, setActiveAction] = useState(null); + const [loading, setLoading] = useState(true); + const router = useRouter(); const addLog = (message: string) => { const timestamp = new Date().toLocaleTimeString(); @@ -88,8 +92,31 @@ export default function AdminToolsPage() { }; useEffect(() => { - loadStatus(); - }, []); + async function checkAccess() { + const supabase = createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + router.push('/auth/login'); + return; + } + const { data: userData } = await supabase + .from('users') + .select('role') + .eq('id', user.id) + .single(); + if (!userData || userData.role === 'verwaltung') { + router.push('/admin'); + return; + } + setLoading(false); + loadStatus(); + } + checkAccess(); + }, [router]); + + if (loading) { + return
; + } return (
diff --git a/shop/components/admin/user-dialog.tsx b/shop/components/admin/user-dialog.tsx index d97e12a..28414a3 100644 --- a/shop/components/admin/user-dialog.tsx +++ b/shop/components/admin/user-dialog.tsx @@ -30,7 +30,7 @@ import { Plus, Building2 } from 'lucide-react' const userSchema = z.object({ email: z.string().email('Ungültige E-Mail-Adresse'), - role: z.enum(['admin', 'partner']), + role: z.enum(['admin', 'partner', 'verwaltung']), company_id: z.string().optional().or(z.literal('')), first_name: z.string().optional().or(z.literal('')), last_name: z.string().optional().or(z.literal('')), @@ -185,6 +185,7 @@ export function UserDialog({ companies }: UserDialogProps) { > + diff --git a/shop/components/admin/user-list.tsx b/shop/components/admin/user-list.tsx index 2569c1b..12d7fcc 100644 --- a/shop/components/admin/user-list.tsx +++ b/shop/components/admin/user-list.tsx @@ -17,17 +17,19 @@ import { deleteUser, resetPassword, updateUserRole, updateUserProfile } from '@/ import { assignCompanyToUser } from '@/lib/actions/companies' import { Badge } from '@/components/ui/badge' -type Role = 'admin' | 'partner' | 'gesperrt' +type Role = 'admin' | 'partner' | 'verwaltung' | 'gesperrt' const ROLE_LABELS: Record = { admin: 'Admin', partner: 'Partner', + verwaltung: 'Verwaltung', 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', + verwaltung: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30', gesperrt: 'bg-red-500/20 text-red-400 border border-red-500/30', } @@ -205,7 +207,7 @@ export function UserList({ initialUsers, companies }: UserListProps) { {displayUsers.map((user: any) => { - const role: Role = (['admin', 'partner', 'gesperrt'].includes(user.role) ? user.role : 'partner') as Role + const role: Role = (['admin', 'partner', 'verwaltung', 'gesperrt'].includes(user.role) ? user.role : 'partner') as Role const isLoadingRole = loadingId === user.id + '-role' const isLoadingCompany = loadingId === user.id + '-company' const isLoadingDelete = loadingId === user.id @@ -288,14 +290,17 @@ export function UserList({ initialUsers, companies }: UserListProps) { onChange={(e) => handleRoleChange(user.id, e.target.value as Role)} className={`text-xs font-semibold rounded px-3 py-1.5 cursor-pointer focus:outline-none focus:ring-2 transition-colors bg-white dark:bg-slate-950/80 ${role === 'admin' ? 'text-purple-600 dark:text-purple-300 border border-purple-500/40 focus:ring-purple-500/50' - : role === 'gesperrt' - ? 'text-red-600 dark:text-red-300 border border-red-500/40 focus:ring-red-500/50' - : 'text-blue-600 dark:text-blue-300 border border-blue-500/40 focus:ring-blue-500/50' + : role === 'verwaltung' + ? 'text-emerald-600 dark:text-emerald-300 border border-emerald-500/40 focus:ring-emerald-500/50' + : role === 'gesperrt' + ? 'text-red-600 dark:text-red-300 border border-red-500/40 focus:ring-red-500/50' + : 'text-blue-600 dark:text-blue-300 border border-blue-500/40 focus:ring-blue-500/50' }`} style={{ appearance: 'auto' }} > + )} diff --git a/shop/lib/actions/users.ts b/shop/lib/actions/users.ts index 9cb7895..7170808 100644 --- a/shop/lib/actions/users.ts +++ b/shop/lib/actions/users.ts @@ -49,7 +49,7 @@ export async function getUsers() { })); } -export async function createUser(data: { email: string; role?: 'admin' | 'partner'; email_confirm?: boolean; company_id?: string; first_name?: string; last_name?: string }) { +export async function createUser(data: { email: string; role?: 'admin' | 'partner' | 'verwaltung'; email_confirm?: boolean; company_id?: string; first_name?: string; last_name?: string }) { const admin = createAdminClient(); const { data: { user }, error } = await admin.auth.admin.createUser({ email: data.email, @@ -152,7 +152,7 @@ export async function deleteUser(id: string) { } } -export async function updateUserRole(id: string, role: 'admin' | 'partner' | 'gesperrt') { +export async function updateUserRole(id: string, role: 'admin' | 'partner' | 'verwaltung' | 'gesperrt') { const admin = createAdminClient(); const { error } = await admin .from('users')