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