feat(role): add 'verwaltung' role with gated access to settings and tools
All checks were successful
Staging Build / build (push) Successful in 3m26s

This commit is contained in:
DanielS
2026-07-03 10:10:57 +02:00
parent ca07dd0079
commit 4f69fd7e44
7 changed files with 129 additions and 41 deletions

View File

@@ -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<File | null>(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 <div className="p-8 text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>;
}
return (
<div className="p-6 max-w-4xl mx-auto text-slate-900 dark:text-white space-y-8">
{/* Page Header */}

View File

@@ -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 (
<div className="flex min-h-screen bg-slate-50 dark:bg-[#020617] text-slate-900 dark:text-white">
{/* Sidebar */}
@@ -64,21 +67,25 @@ export default async function AdminLayout({
Firmen
</Link>
<div className="pt-2 pb-1 px-3">
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-600">Einstellungen</span>
</div>
<Link href="/admin/einstellungen" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all">
<Settings className="w-5 h-5" />
Allgemein
</Link>
<Link href="/admin/settings" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all">
<Wrench className="w-5 h-5" />
SMTP
</Link>
<Link href="/admin/tools" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all">
<Database className="w-5 h-5" />
DB-Tools
</Link>
{!isVerwaltung && (
<>
<div className="pt-2 pb-1 px-3">
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-600">Einstellungen</span>
</div>
<Link href="/admin/einstellungen" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all">
<Settings className="w-5 h-5" />
Allgemein
</Link>
<Link href="/admin/settings" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all">
<Wrench className="w-5 h-5" />
SMTP
</Link>
<Link href="/admin/tools" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all">
<Database className="w-5 h-5" />
DB-Tools
</Link>
</>
)}
</nav>
<div className="p-4 border-t border-slate-200 dark:border-white/5 space-y-4">

View File

@@ -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<HTMLInputElement>) => {
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 <div className="p-8">Loading</div>;
if (loading) return <div className="p-8 text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>;
return (
<div className="max-w-2xl mx-auto p-8 bg-black/30 backdrop-blur-xl rounded-lg text-white">

View File

@@ -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<string[]>([]);
const [activeAction, setActiveAction] = useState<string | null>(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 <div className="min-h-screen bg-[#020617] text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>;
}
return (
<div className="min-h-screen bg-[#020617] text-white px-4 py-12 relative overflow-hidden">

View File

@@ -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) {
>
<option value="partner" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Partner</option>
<option value="admin" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Admin</option>
<option value="verwaltung" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Verwaltung</option>
</select>
</FormControl>
<FormMessage />

View File

@@ -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<Role, string> = {
admin: 'Admin',
partner: 'Partner',
verwaltung: 'Verwaltung',
gesperrt: 'Gesperrt',
}
const ROLE_BADGE_CLASSES: Record<Role, string> = {
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) {
</TableHeader>
<TableBody>
{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' }}
>
<option value="admin" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Admin</option>
<option value="partner" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Partner</option>
<option value="verwaltung" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Verwaltung</option>
<option value="gesperrt" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Gesperrt</option>
</select>
)}

View File

@@ -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')