Compare commits
6 Commits
b8420cc1ee
...
4f69fd7e44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f69fd7e44 | ||
|
|
ca07dd0079 | ||
|
|
233b9f135d | ||
|
|
6dc1b1c00c | ||
|
|
f5f577ea73 | ||
|
|
250dd82847 |
@@ -1,35 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Initialize Supabase if needed and ensure default admin user exists
|
echo "Supabase default user creation disabled – setup wizard will handle user initialization."
|
||||||
# Requires environment variables:
|
|
||||||
# SUPABASE_URL – base URL of Supabase instance (e.g., http://192.168.1.132:8000)
|
|
||||||
# SUPABASE_SERVICE_ROLE_KEY – service role secret key with admin privileges
|
|
||||||
# DEFAULT_USER_EMAIL – email for the default user (default: info@hephex.de)
|
|
||||||
# DEFAULT_USER_PASSWORD – password for the default user (default: Kathi2022!)
|
|
||||||
|
|
||||||
DEFAULT_EMAIL="${DEFAULT_USER_EMAIL:-info@hephex.de}"
|
|
||||||
DEFAULT_PASSWORD="${DEFAULT_USER_PASSWORD:-Kathi2022!}"
|
|
||||||
|
|
||||||
if [[ -z "$SUPABASE_URL" || -z "$SUPABASE_SERVICE_ROLE_KEY" ]]; then
|
|
||||||
echo "Supabase URL or Service Role Key not set – skipping user creation."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the user already exists
|
|
||||||
EXISTING=$(curl -s "${SUPABASE_URL}/auth/v1/admin/users?apikey=${SUPABASE_SERVICE_ROLE_KEY}" \
|
|
||||||
-H "Authorization: Bearer ${SUPABASE_SERVICE_ROLE_KEY}" \
|
|
||||||
-H "Content-Type: application/json" | grep -i "${DEFAULT_EMAIL}")
|
|
||||||
|
|
||||||
if [[ -n "$EXISTING" ]]; then
|
|
||||||
echo "Default user ${DEFAULT_EMAIL} already exists."
|
|
||||||
else
|
|
||||||
echo "Creating default user ${DEFAULT_EMAIL}..."
|
|
||||||
curl -s -X POST "${SUPABASE_URL}/auth/v1/admin/users?apikey=${SUPABASE_SERVICE_ROLE_KEY}" \
|
|
||||||
-H "Authorization: Bearer ${SUPABASE_SERVICE_ROLE_KEY}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"email\": \"${DEFAULT_EMAIL}\", \"password\": \"${DEFAULT_PASSWORD}\", \"email_confirmed\": true}"
|
|
||||||
echo "User created."
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
const state = localStorage.getItem('demo_banner_disabled') !== 'true';
|
async function checkAccess() {
|
||||||
setDemoActive(state);
|
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) => {
|
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,21 +67,25 @@ export default async function AdminLayout({
|
|||||||
Firmen
|
Firmen
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="pt-2 pb-1 px-3">
|
{!isVerwaltung && (
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-600">Einstellungen</span>
|
<>
|
||||||
</div>
|
<div className="pt-2 pb-1 px-3">
|
||||||
<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">
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-600">Einstellungen</span>
|
||||||
<Settings className="w-5 h-5" />
|
</div>
|
||||||
Allgemein
|
<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">
|
||||||
</Link>
|
<Settings className="w-5 h-5" />
|
||||||
<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">
|
Allgemein
|
||||||
<Wrench className="w-5 h-5" />
|
</Link>
|
||||||
SMTP
|
<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">
|
||||||
</Link>
|
<Wrench className="w-5 h-5" />
|
||||||
<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">
|
SMTP
|
||||||
<Database className="w-5 h-5" />
|
</Link>
|
||||||
DB-Tools
|
<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">
|
||||||
</Link>
|
<Database className="w-5 h-5" />
|
||||||
|
DB-Tools
|
||||||
|
</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,17 +19,36 @@ 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(() => {
|
||||||
fetch('/api/admin/smtp-settings')
|
async function checkAccessAndLoad() {
|
||||||
.then((res) => res.json())
|
const supabase = createClient();
|
||||||
.then((data) => {
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
if (data.settings) setSettings(data.settings as Settings);
|
if (!user) {
|
||||||
else setMessage('Failed to load settings');
|
router.push('/auth/login');
|
||||||
})
|
return;
|
||||||
.catch(() => setMessage('Failed to load settings'))
|
}
|
||||||
.finally(() => setLoading(false));
|
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 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(() => {
|
||||||
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 (
|
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">
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { LoginForm } from "@/components/login-form";
|
import { LoginForm } from "@/components/login-form";
|
||||||
|
import { isSetupNeeded } from "@/lib/actions/setup";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
if (await isSetupNeeded()) {
|
||||||
|
redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function CustomerDetailPage({ params }: { params: Promise<{ id: s
|
|||||||
const [showGdprConfirm, setShowGdprConfirm] = useState(false)
|
const [showGdprConfirm, setShowGdprConfirm] = useState(false)
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', email: '',
|
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', email: '',
|
||||||
|
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,6 +39,10 @@ export default function CustomerDetailPage({ params }: { params: Promise<{ id: s
|
|||||||
zip: c.zip ?? '',
|
zip: c.zip ?? '',
|
||||||
city: c.city ?? '',
|
city: c.city ?? '',
|
||||||
email: c.email ?? '',
|
email: c.email ?? '',
|
||||||
|
bank_iban: c.bank_iban ?? '',
|
||||||
|
bank_bic: c.bank_bic ?? '',
|
||||||
|
bank_name: c.bank_name ?? '',
|
||||||
|
bank_owner: c.bank_owner ?? '',
|
||||||
})
|
})
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
@@ -126,6 +131,10 @@ export default function CustomerDetailPage({ params }: { params: Promise<{ id: s
|
|||||||
{ label: 'Straße & Hausnummer', key: 'street', span: true },
|
{ label: 'Straße & Hausnummer', key: 'street', span: true },
|
||||||
{ label: 'PLZ', key: 'zip', span: false },
|
{ label: 'PLZ', key: 'zip', span: false },
|
||||||
{ label: 'Ort', key: 'city', span: false },
|
{ label: 'Ort', key: 'city', span: false },
|
||||||
|
{ label: 'Kontoinhaber', key: 'bank_owner', span: false },
|
||||||
|
{ label: 'IBAN', key: 'bank_iban', span: false },
|
||||||
|
{ label: 'BIC', key: 'bank_bic', span: false },
|
||||||
|
{ label: 'Bankname', key: 'bank_name', span: false },
|
||||||
] as const).map(({ label, key, span }) => (
|
] as const).map(({ label, key, span }) => (
|
||||||
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
|
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
|
||||||
<Label className="text-slate-300 text-sm">{label}</Label>
|
<Label className="text-slate-300 text-sm">{label}</Label>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function NewCustomerPage() {
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', email: '',
|
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', email: '',
|
||||||
|
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -62,6 +63,10 @@ export default function NewCustomerPage() {
|
|||||||
{ label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: 'Musterstraße 1' },
|
{ label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: 'Musterstraße 1' },
|
||||||
{ label: 'PLZ', key: 'zip', span: false, placeholder: '12345' },
|
{ label: 'PLZ', key: 'zip', span: false, placeholder: '12345' },
|
||||||
{ label: 'Ort', key: 'city', span: false, placeholder: 'Musterstadt' },
|
{ label: 'Ort', key: 'city', span: false, placeholder: 'Musterstadt' },
|
||||||
|
{ label: 'Kontoinhaber', key: 'bank_owner', span: false, placeholder: 'Max Mustermann' },
|
||||||
|
{ label: 'IBAN', key: 'bank_iban', span: false, placeholder: 'DE89370400440532013000' },
|
||||||
|
{ label: 'BIC', key: 'bank_bic', span: false, placeholder: 'SOLADE21XXX' },
|
||||||
|
{ label: 'Bankname', key: 'bank_name', span: false, placeholder: 'Musterbank' },
|
||||||
] as const).map(({ label, key, span, placeholder }) => (
|
] as const).map(({ label, key, span, placeholder }) => (
|
||||||
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
|
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
|
||||||
<Label className="text-slate-300 text-sm">{label}</Label>
|
<Label className="text-slate-300 text-sm">{label}</Label>
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ export default async function OrderSuccessPage({
|
|||||||
<p className="font-semibold text-white">{o.customer_data?.company_name}</p>
|
<p className="font-semibold text-white">{o.customer_data?.company_name}</p>
|
||||||
<p>{o.customer_data?.first_name} {o.customer_data?.last_name}</p>
|
<p>{o.customer_data?.first_name} {o.customer_data?.last_name}</p>
|
||||||
<p>{o.customer_data?.address}, {o.customer_data?.zip_code} {o.customer_data?.city}</p>
|
<p>{o.customer_data?.address}, {o.customer_data?.zip_code} {o.customer_data?.city}</p>
|
||||||
|
{o.customer_data?.bank_iban && (
|
||||||
|
<div className="text-xs text-slate-500 pt-2 border-t border-white/5 mt-2 space-y-0.5">
|
||||||
|
<p><span className="font-semibold text-slate-400">Kontoinhaber:</span> {o.customer_data.bank_owner}</p>
|
||||||
|
<p><span className="font-semibold text-slate-400">IBAN:</span> {o.customer_data.bank_iban}</p>
|
||||||
|
{o.customer_data.bank_bic && <p><span className="font-semibold text-slate-400">BIC:</span> {o.customer_data.bank_bic}</p>}
|
||||||
|
{o.customer_data.bank_name && <p><span className="font-semibold text-slate-400">Bank:</span> {o.customer_data.bank_name}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PDF-Download */}
|
{/* PDF-Download */}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
import { HomeClient } from '@/components/HomeClient'
|
import { HomeClient } from '@/components/HomeClient'
|
||||||
|
import { isSetupNeeded } from '@/lib/actions/setup'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
|
if (await isSetupNeeded()) {
|
||||||
|
redirect('/setup')
|
||||||
|
}
|
||||||
|
|
||||||
let user = null
|
let user = null
|
||||||
try {
|
try {
|
||||||
const supabase = await createClient()
|
const supabase = await createClient()
|
||||||
|
|||||||
15
shop/app/setup/page.tsx
Normal file
15
shop/app/setup/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { isSetupNeeded } from '@/lib/actions/setup'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
import { SetupWizard } from '@/components/SetupWizard'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
export default async function SetupPage() {
|
||||||
|
const needed = await isSetupNeeded()
|
||||||
|
|
||||||
|
if (!needed) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SetupWizard />
|
||||||
|
}
|
||||||
@@ -11,8 +11,9 @@ interface HeaderWrapperProps {
|
|||||||
export function HeaderWrapper({ navbar, children }: HeaderWrapperProps) {
|
export function HeaderWrapper({ navbar, children }: HeaderWrapperProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isAdmin = pathname?.startsWith("/admin");
|
const isAdmin = pathname?.startsWith("/admin");
|
||||||
|
const isSetup = pathname === "/setup";
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin || isSetup) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { type User } from '@supabase/supabase-js'
|
|||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ShoppingCart,
|
MessageSquare,
|
||||||
Utensils,
|
BookOpen,
|
||||||
Layers,
|
Layers,
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
Users,
|
Users,
|
||||||
@@ -244,7 +244,7 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
|||||||
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-blue-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-blue-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
<div className="space-y-4 relative">
|
<div className="space-y-4 relative">
|
||||||
<div className="inline-flex p-3 bg-blue-500/10 rounded-xl text-blue-400 group-hover:bg-blue-500/20 group-hover:scale-110 transition-all duration-300">
|
<div className="inline-flex p-3 bg-blue-500/10 rounded-xl text-blue-400 group-hover:bg-blue-500/20 group-hover:scale-110 transition-all duration-300">
|
||||||
<ShoppingCart className="h-6 w-6" />
|
<MessageSquare className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Zulip</h3>
|
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Zulip</h3>
|
||||||
<p className="text-slate-400 text-sm leading-relaxed">
|
<p className="text-slate-400 text-sm leading-relaxed">
|
||||||
@@ -262,7 +262,7 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
|||||||
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-indigo-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-indigo-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
<div className="space-y-4 relative">
|
<div className="space-y-4 relative">
|
||||||
<div className="inline-flex p-3 bg-indigo-500/10 rounded-xl text-indigo-400 group-hover:bg-indigo-500/20 group-hover:scale-110 transition-all duration-300">
|
<div className="inline-flex p-3 bg-indigo-500/10 rounded-xl text-indigo-400 group-hover:bg-indigo-500/20 group-hover:scale-110 transition-all duration-300">
|
||||||
<Utensils className="h-6 w-6" />
|
<BookOpen className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Wiki</h3>
|
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Wiki</h3>
|
||||||
<p className="text-slate-400 text-sm leading-relaxed">
|
<p className="text-slate-400 text-sm leading-relaxed">
|
||||||
|
|||||||
406
shop/components/SetupWizard.tsx
Normal file
406
shop/components/SetupWizard.tsx
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { Sparkles, ShieldCheck, Mail, ArrowRight, Loader2, Check, Lock, Building, User } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { createClient } from '@/lib/supabase/client'
|
||||||
|
import { completeSetup } from '@/lib/actions/setup'
|
||||||
|
|
||||||
|
export function SetupWizard() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [step, setStep] = useState(1)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
|
||||||
|
// Step 2 Form: Admin account
|
||||||
|
const [adminForm, setAdminForm] = useState({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
companyName: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 3 Form: SMTP Config
|
||||||
|
const [smtpForm, setSmtpForm] = useState({
|
||||||
|
host: '',
|
||||||
|
port: '587',
|
||||||
|
secure: false,
|
||||||
|
user: '',
|
||||||
|
pass: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validation checks for Admin form
|
||||||
|
const isAdminFormValid =
|
||||||
|
adminForm.email.includes('@') &&
|
||||||
|
adminForm.password.length >= 6 &&
|
||||||
|
adminForm.password === adminForm.confirmPassword &&
|
||||||
|
adminForm.companyName.trim().length > 0 &&
|
||||||
|
adminForm.firstName.trim().length > 0 &&
|
||||||
|
adminForm.lastName.trim().length > 0
|
||||||
|
|
||||||
|
// Validation checks for SMTP form
|
||||||
|
const isSmtpFormValid =
|
||||||
|
smtpForm.host.trim().length > 0 &&
|
||||||
|
!isNaN(Number(smtpForm.port)) &&
|
||||||
|
smtpForm.user.trim().length > 0
|
||||||
|
|
||||||
|
const handleFinishSetup = async () => {
|
||||||
|
if (!isAdminFormValid || !isSmtpFormValid) return
|
||||||
|
setLoading(true)
|
||||||
|
setErrorMsg('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Submit details via server action
|
||||||
|
const res = await completeSetup(
|
||||||
|
{
|
||||||
|
email: adminForm.email,
|
||||||
|
password: adminForm.password,
|
||||||
|
companyName: adminForm.companyName,
|
||||||
|
firstName: adminForm.firstName,
|
||||||
|
lastName: adminForm.lastName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host: smtpForm.host,
|
||||||
|
port: Number(smtpForm.port),
|
||||||
|
secure: smtpForm.secure,
|
||||||
|
user: smtpForm.user,
|
||||||
|
pass: smtpForm.pass,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!res.success) {
|
||||||
|
setErrorMsg(res.error || 'Fehler beim Setup.')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Automatischer Login für flüssiges Erlebnis
|
||||||
|
const supabase = createClient()
|
||||||
|
const { error: loginError } = await supabase.auth.signInWithPassword({
|
||||||
|
email: adminForm.email,
|
||||||
|
password: adminForm.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loginError) {
|
||||||
|
console.error('Auto login failed:', loginError)
|
||||||
|
// Redirect anyway since setup is done
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Weiterleitung
|
||||||
|
router.push('/')
|
||||||
|
router.refresh()
|
||||||
|
} catch (e: any) {
|
||||||
|
setErrorMsg(e.message || 'Ein unerwarteter Fehler ist aufgetreten.')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#020617] text-white flex flex-col items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
{/* Dynamic Background Glow */}
|
||||||
|
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] rounded-full bg-blue-500/10 blur-[120px]" />
|
||||||
|
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] rounded-full bg-purple-500/10 blur-[120px]" />
|
||||||
|
|
||||||
|
<div className="w-full max-w-xl relative z-10">
|
||||||
|
{/* Step Indicator Header */}
|
||||||
|
<div className="flex items-center justify-between mb-8 px-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xl font-bold tracking-tight text-gradient bg-gradient-to-r from-blue-400 to-indigo-400">CASPOS</span>
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20 font-medium">Initialisierung</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
{[1, 2, 3].map((s) => (
|
||||||
|
<div
|
||||||
|
key={s}
|
||||||
|
className={`h-1.5 rounded-full transition-all duration-300 ${
|
||||||
|
s === step ? 'w-8 bg-blue-500' : 'w-2 bg-slate-800'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{step === 1 && (
|
||||||
|
<motion.div
|
||||||
|
key="step1"
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -15 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 mx-auto shadow-inner">
|
||||||
|
<Sparkles className="w-8 h-8 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<h1 className="text-3xl font-extrabold tracking-tight">Willkommen bei CASPOS!</h1>
|
||||||
|
<p className="text-slate-400 leading-relaxed">
|
||||||
|
Richten Sie Ihren persönlichen Lizenz- und Angebots-Shop in wenigen Schritten ein. Wir konfigurieren Ihr Administrator-Konto und die Mailverbindung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-xl bg-white/5 border border-white/5 text-sm text-slate-400 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Check className="w-4 h-4 text-green-400 shrink-0" />
|
||||||
|
<span>Komplett self-hosted & sicher</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Check className="w-4 h-4 text-green-400 shrink-0" />
|
||||||
|
<span>Automatische Schema-Updates auf dem neuesten Stand</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Check className="w-4 h-4 text-green-400 shrink-0" />
|
||||||
|
<span>SMTP Mailversand für direkte Angebotsbestätigungen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => setStep(2)}
|
||||||
|
className="w-full py-6 text-base font-bold bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 shadow-lg shadow-blue-500/15 group"
|
||||||
|
>
|
||||||
|
Setup starten
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<motion.div
|
||||||
|
key="step2"
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -15 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<ShieldCheck className="w-6 h-6 text-blue-400" />
|
||||||
|
Admin-Konto anlegen
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-400 text-sm">
|
||||||
|
Erstellen Sie den ersten Administrator-Benutzer. Dieser erhält vollen Zugriff auf das System.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">Vorname *</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<Input
|
||||||
|
value={adminForm.firstName}
|
||||||
|
onChange={(e) => setAdminForm({ ...adminForm, firstName: e.target.value })}
|
||||||
|
placeholder="Vorname"
|
||||||
|
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">Nachname *</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<Input
|
||||||
|
value={adminForm.lastName}
|
||||||
|
onChange={(e) => setAdminForm({ ...adminForm, lastName: e.target.value })}
|
||||||
|
placeholder="Nachname"
|
||||||
|
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">Unternehmen / Partnername *</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Building className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<Input
|
||||||
|
value={adminForm.companyName}
|
||||||
|
onChange={(e) => setAdminForm({ ...adminForm, companyName: e.target.value })}
|
||||||
|
placeholder="Name Ihrer Firma"
|
||||||
|
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">E-Mail-Adresse *</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={adminForm.email}
|
||||||
|
onChange={(e) => setAdminForm({ ...adminForm, email: e.target.value })}
|
||||||
|
placeholder="admin@firma.de"
|
||||||
|
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">Passwort * (min. 6 Zeichen)</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={adminForm.password}
|
||||||
|
onChange={(e) => setAdminForm({ ...adminForm, password: e.target.value })}
|
||||||
|
placeholder="••••••"
|
||||||
|
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">Passwort bestätigen *</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={adminForm.confirmPassword}
|
||||||
|
onChange={(e) => setAdminForm({ ...adminForm, confirmPassword: e.target.value })}
|
||||||
|
placeholder="••••••"
|
||||||
|
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button variant="ghost" onClick={() => setStep(1)} className="text-white hover:bg-white/5">
|
||||||
|
Zurück
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setStep(3)}
|
||||||
|
disabled={!isAdminFormValid}
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-bold"
|
||||||
|
>
|
||||||
|
Weiter zu SMTP
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
|
<motion.div
|
||||||
|
key="step3"
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -15 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<Mail className="w-6 h-6 text-blue-400" />
|
||||||
|
SMTP-Mailserver konfigurieren
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-400 text-sm">
|
||||||
|
Tragen Sie Ihre Mailserver-Daten ein, um automatisierte E-Mails an Partner und Kunden senden zu können.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-xs">
|
||||||
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="col-span-2 space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">SMTP Host *</Label>
|
||||||
|
<Input
|
||||||
|
value={smtpForm.host}
|
||||||
|
onChange={(e) => setSmtpForm({ ...smtpForm, host: e.target.value })}
|
||||||
|
placeholder="smtp.domain.de"
|
||||||
|
className="bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">SMTP Port *</Label>
|
||||||
|
<Input
|
||||||
|
value={smtpForm.port}
|
||||||
|
onChange={(e) => setSmtpForm({ ...smtpForm, port: e.target.value })}
|
||||||
|
placeholder="587"
|
||||||
|
className="bg-white/5 border-white/10 text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">Benutzername *</Label>
|
||||||
|
<Input
|
||||||
|
value={smtpForm.user}
|
||||||
|
onChange={(e) => setSmtpForm({ ...smtpForm, user: e.target.value })}
|
||||||
|
placeholder="mail@domain.de"
|
||||||
|
className="bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-slate-300 text-xs">Passwort</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={smtpForm.pass}
|
||||||
|
onChange={(e) => setSmtpForm({ ...smtpForm, pass: e.target.value })}
|
||||||
|
placeholder="••••••••••••"
|
||||||
|
className="bg-white/5 border-white/10 text-white placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 pt-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="smtp_secure"
|
||||||
|
checked={smtpForm.secure}
|
||||||
|
onChange={(e) => setSmtpForm({ ...smtpForm, secure: e.target.checked })}
|
||||||
|
className="w-4 h-4 rounded border-slate-700 bg-white/5 text-blue-500 focus:ring-0 focus:ring-offset-0"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="smtp_secure" className="text-slate-300 text-sm cursor-pointer select-none">
|
||||||
|
Sichere Verbindung (SSL/TLS anstelle STARTTLS)
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setStep(2)}
|
||||||
|
disabled={loading}
|
||||||
|
className="text-white hover:bg-white/5"
|
||||||
|
>
|
||||||
|
Zurück
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleFinishSetup}
|
||||||
|
disabled={!isSmtpFormValid || loading}
|
||||||
|
className="flex-1 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> Setup wird abgeschlossen...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Einrichtung abschließen'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,14 +290,17 @@ 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 === 'gesperrt'
|
: role === 'verwaltung'
|
||||||
? 'text-red-600 dark:text-red-300 border border-red-500/40 focus:ring-red-500/50'
|
? 'text-emerald-600 dark:text-emerald-300 border border-emerald-500/40 focus:ring-emerald-500/50'
|
||||||
: 'text-blue-600 dark:text-blue-300 border border-blue-500/40 focus:ring-blue-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' }}
|
style={{ appearance: 'auto' }}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => {
|
|||||||
<Text>{customer.address}</Text>
|
<Text>{customer.address}</Text>
|
||||||
<Text>{customer.zip_code} {customer.city}</Text>
|
<Text>{customer.zip_code} {customer.city}</Text>
|
||||||
<Text>USt-IdNr: {customer.vat_id}</Text>
|
<Text>USt-IdNr: {customer.vat_id}</Text>
|
||||||
|
{customer.bank_iban && (
|
||||||
|
<Text style={{ marginTop: 4, color: '#666' }}>
|
||||||
|
Bank: {customer.bank_name || '-'} · IBAN: {customer.bank_iban} · BIC: {customer.bank_bic || '-'} · Inhaber: {customer.bank_owner || '-'}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export function OrderWizard({
|
|||||||
const [isCreatingCustomer, setIsCreatingCustomer] = useState(false)
|
const [isCreatingCustomer, setIsCreatingCustomer] = useState(false)
|
||||||
const [newCustomerForm, setNewCustomerForm] = useState({
|
const [newCustomerForm, setNewCustomerForm] = useState({
|
||||||
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '',
|
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '',
|
||||||
|
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Abrechnungsmodell-Auswahl ('one_time' = Kauf, 'monthly' = Abo)
|
// Abrechnungsmodell-Auswahl ('one_time' = Kauf, 'monthly' = Abo)
|
||||||
@@ -388,7 +389,10 @@ export function OrderWizard({
|
|||||||
setEndCustomers(prev => [...prev, created])
|
setEndCustomers(prev => [...prev, created])
|
||||||
setSelectedEndCustomerId(created.id)
|
setSelectedEndCustomerId(created.id)
|
||||||
setCustomerMode('select')
|
setCustomerMode('select')
|
||||||
setNewCustomerForm({ company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '' })
|
setNewCustomerForm({
|
||||||
|
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '',
|
||||||
|
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Fehler beim Anlegen des Kunden.')
|
alert('Fehler beim Anlegen des Kunden.')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -573,6 +577,10 @@ export function OrderWizard({
|
|||||||
{ label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: '' },
|
{ label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: '' },
|
||||||
{ label: 'PLZ', key: 'zip', span: false, placeholder: '' },
|
{ label: 'PLZ', key: 'zip', span: false, placeholder: '' },
|
||||||
{ label: 'Ort', key: 'city', span: false, placeholder: '' },
|
{ label: 'Ort', key: 'city', span: false, placeholder: '' },
|
||||||
|
{ label: 'Kontoinhaber', key: 'bank_owner', span: false, placeholder: 'Max Mustermann' },
|
||||||
|
{ label: 'IBAN', key: 'bank_iban', span: false, placeholder: 'DE89370400440532013000' },
|
||||||
|
{ label: 'BIC', key: 'bank_bic', span: false, placeholder: 'SOLADE21XXX' },
|
||||||
|
{ label: 'Bankname', key: 'bank_name', span: false, placeholder: 'Musterbank' },
|
||||||
] as const).map(({ label, key, span, placeholder }) => (
|
] as const).map(({ label, key, span, placeholder }) => (
|
||||||
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
|
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
|
||||||
<Label className="text-slate-300 text-sm">{label}</Label>
|
<Label className="text-slate-300 text-sm">{label}</Label>
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ export async function adminCreateEndCustomer(data: {
|
|||||||
zip?: string
|
zip?: string
|
||||||
city?: string
|
city?: string
|
||||||
email?: string
|
email?: string
|
||||||
|
bank_iban?: string
|
||||||
|
bank_bic?: string
|
||||||
|
bank_name?: string
|
||||||
|
bank_owner?: string
|
||||||
}) {
|
}) {
|
||||||
const admin = createAdminClient()
|
const admin = createAdminClient()
|
||||||
const { data: dbData, error } = await admin
|
const { data: dbData, error } = await admin
|
||||||
@@ -188,6 +192,10 @@ export async function adminCreateEndCustomer(data: {
|
|||||||
zip: data.zip || null,
|
zip: data.zip || null,
|
||||||
city: data.city || null,
|
city: data.city || null,
|
||||||
email: data.email || null,
|
email: data.email || null,
|
||||||
|
bank_iban: data.bank_iban || null,
|
||||||
|
bank_bic: data.bank_bic || null,
|
||||||
|
bank_name: data.bank_name || null,
|
||||||
|
bank_owner: data.bank_owner || null,
|
||||||
}])
|
}])
|
||||||
.select()
|
.select()
|
||||||
.single()
|
.single()
|
||||||
@@ -205,6 +213,10 @@ export async function adminUpdateEndCustomer(id: string, data: {
|
|||||||
zip?: string
|
zip?: string
|
||||||
city?: string
|
city?: string
|
||||||
email?: string
|
email?: string
|
||||||
|
bank_iban?: string
|
||||||
|
bank_bic?: string
|
||||||
|
bank_name?: string
|
||||||
|
bank_owner?: string
|
||||||
}) {
|
}) {
|
||||||
const admin = createAdminClient()
|
const admin = createAdminClient()
|
||||||
const { data: dbData, error } = await admin
|
const { data: dbData, error } = await admin
|
||||||
@@ -219,6 +231,10 @@ export async function adminUpdateEndCustomer(id: string, data: {
|
|||||||
zip: data.zip || null,
|
zip: data.zip || null,
|
||||||
city: data.city || null,
|
city: data.city || null,
|
||||||
email: data.email || null,
|
email: data.email || null,
|
||||||
|
bank_iban: data.bank_iban || null,
|
||||||
|
bank_bic: data.bank_bic || null,
|
||||||
|
bank_name: data.bank_name || null,
|
||||||
|
bank_owner: data.bank_owner || null,
|
||||||
})
|
})
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export type EndCustomerFormData = {
|
|||||||
zip?: string
|
zip?: string
|
||||||
city?: string
|
city?: string
|
||||||
email?: string
|
email?: string
|
||||||
|
bank_iban?: string
|
||||||
|
bank_bic?: string
|
||||||
|
bank_name?: string
|
||||||
|
bank_owner?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Lesen ───────────────────────────────────────────────────────────────────
|
// ─── Lesen ───────────────────────────────────────────────────────────────────
|
||||||
@@ -74,6 +78,10 @@ export async function createEndCustomer(formData: EndCustomerFormData): Promise<
|
|||||||
zip: formData.zip || null,
|
zip: formData.zip || null,
|
||||||
city: formData.city || null,
|
city: formData.city || null,
|
||||||
email: formData.email || null,
|
email: formData.email || null,
|
||||||
|
bank_iban: formData.bank_iban || null,
|
||||||
|
bank_bic: formData.bank_bic || null,
|
||||||
|
bank_name: formData.bank_name || null,
|
||||||
|
bank_owner: formData.bank_owner || null,
|
||||||
}])
|
}])
|
||||||
.select()
|
.select()
|
||||||
.single()
|
.single()
|
||||||
@@ -104,6 +112,10 @@ export async function updateEndCustomer(id: string, formData: EndCustomerFormDat
|
|||||||
zip: formData.zip || null,
|
zip: formData.zip || null,
|
||||||
city: formData.city || null,
|
city: formData.city || null,
|
||||||
email: formData.email || null,
|
email: formData.email || null,
|
||||||
|
bank_iban: formData.bank_iban || null,
|
||||||
|
bank_bic: formData.bank_bic || null,
|
||||||
|
bank_name: formData.bank_name || null,
|
||||||
|
bank_owner: formData.bank_owner || null,
|
||||||
})
|
})
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
|
|
||||||
@@ -138,6 +150,10 @@ export async function anonymizeEndCustomer(id: string): Promise<void> {
|
|||||||
zip: null,
|
zip: null,
|
||||||
city: null,
|
city: null,
|
||||||
email: null,
|
email: null,
|
||||||
|
bank_iban: null,
|
||||||
|
bank_bic: null,
|
||||||
|
bank_name: null,
|
||||||
|
bank_owner: null,
|
||||||
is_anonymized: true,
|
is_anonymized: true,
|
||||||
})
|
})
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
|
|||||||
140
shop/lib/actions/setup.ts
Normal file
140
shop/lib/actions/setup.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { createAdminClient } from '@/lib/supabase/admin'
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
|
export type AdminSetupData = {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
companyName: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SmtpSetupData = {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
secure: boolean
|
||||||
|
user: string
|
||||||
|
pass: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if any user exists in the public.users table.
|
||||||
|
* If count is 0, setup is required.
|
||||||
|
*/
|
||||||
|
export async function isSetupNeeded(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const admin = createAdminClient()
|
||||||
|
|
||||||
|
// 1. Check if any user exists in Supabase Auth
|
||||||
|
const { data: authData, error: authError } = await admin.auth.admin.listUsers({
|
||||||
|
perPage: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authError) {
|
||||||
|
console.error('Error checking auth users list:', authError)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.users.length > 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if any user exists in public.users table
|
||||||
|
const { count, error: dbError } = await admin
|
||||||
|
.from('users')
|
||||||
|
.select('*', { count: 'exact', head: true })
|
||||||
|
|
||||||
|
if (dbError) {
|
||||||
|
console.error('Error checking users table status:', dbError)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return count === 0
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Exception checking setup status:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completes the initial setup by creating the first admin user,
|
||||||
|
* updating their role and profile, and storing SMTP settings.
|
||||||
|
*/
|
||||||
|
export async function completeSetup(
|
||||||
|
adminData: AdminSetupData,
|
||||||
|
smtpData: SmtpSetupData
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
// 1. Double check if setup is actually needed to prevent double runs
|
||||||
|
const needed = await isSetupNeeded()
|
||||||
|
if (!needed) {
|
||||||
|
return { success: false, error: 'Setup bereits abgeschlossen.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const admin = createAdminClient()
|
||||||
|
|
||||||
|
// 2. Create user in Supabase Auth
|
||||||
|
const { data: authData, error: authError } = await admin.auth.admin.createUser({
|
||||||
|
email: adminData.email,
|
||||||
|
password: adminData.password,
|
||||||
|
email_confirm: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authError || !authData.user) {
|
||||||
|
return { success: false, error: `Fehler beim Erstellen des Admin-Kontos: ${authError?.message}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = authData.user.id
|
||||||
|
|
||||||
|
// 3. Update user role to admin in public.users
|
||||||
|
const { error: roleError } = await admin
|
||||||
|
.from('users')
|
||||||
|
.update({ role: 'admin' })
|
||||||
|
.eq('id', userId)
|
||||||
|
|
||||||
|
if (roleError) {
|
||||||
|
console.error('Error setting user role to admin:', roleError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update company and name details in public.profiles
|
||||||
|
const { error: profileError } = await admin
|
||||||
|
.from('profiles')
|
||||||
|
.update({
|
||||||
|
company_name: adminData.companyName,
|
||||||
|
first_name: adminData.firstName,
|
||||||
|
last_name: adminData.lastName,
|
||||||
|
email: adminData.email,
|
||||||
|
})
|
||||||
|
.eq('id', userId)
|
||||||
|
|
||||||
|
if (profileError) {
|
||||||
|
console.error('Error updating admin profile:', profileError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Store SMTP configuration in public.settings
|
||||||
|
const { error: smtpError } = await admin
|
||||||
|
.from('settings')
|
||||||
|
.upsert({
|
||||||
|
id: 'smtp',
|
||||||
|
host: smtpData.host,
|
||||||
|
port: smtpData.port,
|
||||||
|
secure: smtpData.secure,
|
||||||
|
user: smtpData.user,
|
||||||
|
pass: smtpData.pass,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (smtpError) {
|
||||||
|
console.error('Error saving SMTP settings:', smtpError)
|
||||||
|
return { success: false, error: `Fehler beim Speichern der SMTP-Einstellungen: ${smtpError.message}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/')
|
||||||
|
return { success: true }
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Exception during completeSetup:', e)
|
||||||
|
return { success: false, error: e.message || 'Unerwarteter Fehler beim Setup.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export function buildCustomerSnapshot(
|
|||||||
address: endCustomer.street ?? '',
|
address: endCustomer.street ?? '',
|
||||||
zip_code: endCustomer.zip ?? '',
|
zip_code: endCustomer.zip ?? '',
|
||||||
city: endCustomer.city ?? '',
|
city: endCustomer.city ?? '',
|
||||||
|
bank_iban: endCustomer.bank_iban ?? undefined,
|
||||||
|
bank_bic: endCustomer.bank_bic ?? undefined,
|
||||||
|
bank_name: endCustomer.bank_name ?? undefined,
|
||||||
|
bank_owner: endCustomer.bank_owner ?? undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ export type EndCustomer = {
|
|||||||
zip: string | null
|
zip: string | null
|
||||||
city: string | null
|
city: string | null
|
||||||
email?: string | null
|
email?: string | null
|
||||||
|
bank_iban: string | null
|
||||||
|
bank_bic: string | null
|
||||||
|
bank_name: string | null
|
||||||
|
bank_owner: string | null
|
||||||
is_anonymized: boolean
|
is_anonymized: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
@@ -100,6 +104,10 @@ export type CustomerSnapshot = {
|
|||||||
zip_code: string
|
zip_code: string
|
||||||
city: string
|
city: string
|
||||||
email?: string
|
email?: string
|
||||||
|
bank_iban?: string
|
||||||
|
bank_bic?: string
|
||||||
|
bank_name?: string
|
||||||
|
bank_owner?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderModuleSnapshot = {
|
export type OrderModuleSnapshot = {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ BEGIN
|
|||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
INSERT INTO public.users (id, role)
|
INSERT INTO public.users (id, role)
|
||||||
VALUES (new.id, CASE WHEN new.email = 'info@hephex.de' THEN 'admin' ELSE 'partner' END)
|
VALUES (new.id, 'partner')
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
@@ -53,7 +53,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;
|
|||||||
|
|
||||||
-- Backfill public.users for existing auth.users
|
-- Backfill public.users for existing auth.users
|
||||||
INSERT INTO public.users (id, role)
|
INSERT INTO public.users (id, role)
|
||||||
SELECT id, CASE WHEN email = 'info@hephex.de' THEN 'admin' ELSE 'partner' END
|
SELECT id, 'partner'
|
||||||
FROM auth.users
|
FROM auth.users
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Migration: Add bank details to end_customers table
|
||||||
|
ALTER TABLE end_customers
|
||||||
|
ADD COLUMN IF NOT EXISTS bank_iban TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS bank_bic TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS bank_name TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS bank_owner TEXT;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Migration: Update user creation trigger function to not assign admin role to info@hephex.de automatically
|
||||||
|
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.profiles (id)
|
||||||
|
VALUES (new.id)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO public.users (id, role)
|
||||||
|
VALUES (new.id, 'partner')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
Reference in New Issue
Block a user