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 { Switch } from '@/components/ui/switch';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Download, Upload, Database, AlertTriangle, Loader2 } from 'lucide-react';
|
import { Download, Upload, Database, AlertTriangle, Loader2 } from 'lucide-react';
|
||||||
|
import { createClient } from '@/lib/supabase/client';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function AdminSettings() {
|
export default function AdminSettings() {
|
||||||
const [demoActive, setDemoActive] = useState(true);
|
const [demoActive, setDemoActive] = useState(true);
|
||||||
@@ -13,11 +15,32 @@ export default function AdminSettings() {
|
|||||||
const [statusMsg, setStatusMsg] = useState('');
|
const [statusMsg, setStatusMsg] = useState('');
|
||||||
const [statusType, setStatusType] = useState<'success' | 'error' | 'info' | ''>('');
|
const [statusType, setStatusType] = useState<'success' | 'error' | 'info' | ''>('');
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
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';
|
const state = localStorage.getItem('demo_banner_disabled') !== 'true';
|
||||||
setDemoActive(state);
|
setDemoActive(state);
|
||||||
}, []);
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkAccess();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const toggleDemo = (checked: boolean) => {
|
const toggleDemo = (checked: boolean) => {
|
||||||
setDemoActive(checked);
|
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 (
|
return (
|
||||||
<div className="p-6 max-w-4xl mx-auto text-slate-900 dark:text-white space-y-8">
|
<div className="p-6 max-w-4xl mx-auto text-slate-900 dark:text-white space-y-8">
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ export default async function AdminLayout({
|
|||||||
.eq('id', user.id)
|
.eq('id', user.id)
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
if (!userData || userData.role !== 'admin') {
|
const userRole = userData?.role
|
||||||
|
if (!userData || (userRole !== 'admin' && userRole !== 'verwaltung')) {
|
||||||
redirect('/')
|
redirect('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isVerwaltung = userRole === 'verwaltung'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-50 dark:bg-[#020617] text-slate-900 dark:text-white">
|
<div className="flex min-h-screen bg-slate-50 dark:bg-[#020617] text-slate-900 dark:text-white">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
@@ -64,6 +67,8 @@ export default async function AdminLayout({
|
|||||||
Firmen
|
Firmen
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{!isVerwaltung && (
|
||||||
|
<>
|
||||||
<div className="pt-2 pb-1 px-3">
|
<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>
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-600">Einstellungen</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +84,8 @@ export default async function AdminLayout({
|
|||||||
<Database className="w-5 h-5" />
|
<Database className="w-5 h-5" />
|
||||||
DB-Tools
|
DB-Tools
|
||||||
</Link>
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="p-4 border-t border-slate-200 dark:border-white/5 space-y-4">
|
<div className="p-4 border-t border-slate-200 dark:border-white/5 space-y-4">
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
// Admin Settings page – view & edit SMTP configuration and send test email
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { createClient } from '@/lib/supabase/client';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -17,8 +19,25 @@ export default function SettingsPage() {
|
|||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Load current settings
|
// Load current settings and verify access
|
||||||
useEffect(() => {
|
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')
|
fetch('/api/admin/smtp-settings')
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -27,7 +46,9 @@ export default function SettingsPage() {
|
|||||||
})
|
})
|
||||||
.catch(() => setMessage('Failed to load settings'))
|
.catch(() => setMessage('Failed to load settings'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}
|
||||||
|
checkAccessAndLoad();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value, type, checked } = e.target;
|
const { name, value, type, checked } = e.target;
|
||||||
@@ -69,7 +90,7 @@ export default function SettingsPage() {
|
|||||||
else setMessage(data.error || 'Failed to send test email');
|
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 (
|
return (
|
||||||
<div className="max-w-2xl mx-auto p-8 bg-black/30 backdrop-blur-xl rounded-lg text-white">
|
<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 { Badge } from "@/components/ui/badge";
|
||||||
import { Loader2, RefreshCw, Wrench, Database, AlertTriangle, CheckCircle2, XCircle, Terminal, ArrowLeft } from "lucide-react";
|
import { Loader2, RefreshCw, Wrench, Database, AlertTriangle, CheckCircle2, XCircle, Terminal, ArrowLeft } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface TableStatus {
|
interface TableStatus {
|
||||||
exists: boolean;
|
exists: boolean;
|
||||||
@@ -34,6 +36,8 @@ export default function AdminToolsPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [logs, setLogs] = useState<string[]>([]);
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
const [activeAction, setActiveAction] = useState<string | null>(null);
|
const [activeAction, setActiveAction] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const addLog = (message: string) => {
|
const addLog = (message: string) => {
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
@@ -88,8 +92,31 @@ export default function AdminToolsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
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();
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-[#020617] text-white px-4 py-12 relative overflow-hidden">
|
<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({
|
const userSchema = z.object({
|
||||||
email: z.string().email('Ungültige E-Mail-Adresse'),
|
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('')),
|
company_id: z.string().optional().or(z.literal('')),
|
||||||
first_name: z.string().optional().or(z.literal('')),
|
first_name: z.string().optional().or(z.literal('')),
|
||||||
last_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="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="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>
|
</select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -17,17 +17,19 @@ import { deleteUser, resetPassword, updateUserRole, updateUserProfile } from '@/
|
|||||||
import { assignCompanyToUser } from '@/lib/actions/companies'
|
import { assignCompanyToUser } from '@/lib/actions/companies'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
type Role = 'admin' | 'partner' | 'gesperrt'
|
type Role = 'admin' | 'partner' | 'verwaltung' | 'gesperrt'
|
||||||
|
|
||||||
const ROLE_LABELS: Record<Role, string> = {
|
const ROLE_LABELS: Record<Role, string> = {
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
partner: 'Partner',
|
partner: 'Partner',
|
||||||
|
verwaltung: 'Verwaltung',
|
||||||
gesperrt: 'Gesperrt',
|
gesperrt: 'Gesperrt',
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROLE_BADGE_CLASSES: Record<Role, string> = {
|
const ROLE_BADGE_CLASSES: Record<Role, string> = {
|
||||||
admin: 'bg-purple-500/20 text-purple-400 border border-purple-500/30',
|
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',
|
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',
|
gesperrt: 'bg-red-500/20 text-red-400 border border-red-500/30',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +207,7 @@ export function UserList({ initialUsers, companies }: UserListProps) {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{displayUsers.map((user: any) => {
|
{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 isLoadingRole = loadingId === user.id + '-role'
|
||||||
const isLoadingCompany = loadingId === user.id + '-company'
|
const isLoadingCompany = loadingId === user.id + '-company'
|
||||||
const isLoadingDelete = loadingId === user.id
|
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)}
|
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'
|
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'
|
? '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'
|
: role === 'gesperrt'
|
||||||
? 'text-red-600 dark:text-red-300 border border-red-500/40 focus:ring-red-500/50'
|
? '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'
|
: '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="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="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>
|
<option value="gesperrt" className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">Gesperrt</option>
|
||||||
</select>
|
</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 admin = createAdminClient();
|
||||||
const { data: { user }, error } = await admin.auth.admin.createUser({
|
const { data: { user }, error } = await admin.auth.admin.createUser({
|
||||||
email: data.email,
|
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 admin = createAdminClient();
|
||||||
const { error } = await admin
|
const { error } = await admin
|
||||||
.from('users')
|
.from('users')
|
||||||
|
|||||||
Reference in New Issue
Block a user