feat(role): add 'verwaltung' role with gated access to settings and tools
All checks were successful
Staging Build / build (push) Successful in 3m26s
All checks were successful
Staging Build / build (push) Successful in 3m26s
This commit is contained in:
@@ -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(() => {
|
||||
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 */}
|
||||
|
||||
@@ -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,6 +67,8 @@ export default async function AdminLayout({
|
||||
Firmen
|
||||
</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>
|
||||
@@ -79,6 +84,8 @@ export default async function AdminLayout({
|
||||
<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">
|
||||
|
||||
@@ -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,8 +19,25 @@ export default function SettingsPage() {
|
||||
const [message, setMessage] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
// Load current settings
|
||||
// Load current settings and verify access
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
@@ -27,7 +46,9 @@ export default function SettingsPage() {
|
||||
})
|
||||
.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">
|
||||
|
||||
@@ -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(() => {
|
||||
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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,6 +290,8 @@ 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 === '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'
|
||||
@@ -296,6 +300,7 @@ export function UserList({ initialUsers, companies }: UserListProps) {
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user