From 6dc1b1c00ca8369b2923f887ef336982c679c64f Mon Sep 17 00:00:00 2001 From: DanielS Date: Fri, 3 Jul 2026 09:55:24 +0200 Subject: [PATCH] feat(setup): add initial setup wizard for admin account and SMTP configuration --- shop/app/auth/login/page.tsx | 8 +- shop/app/page.tsx | 6 + shop/app/setup/page.tsx | 15 ++ shop/components/HeaderWrapper.tsx | 3 +- shop/components/SetupWizard.tsx | 406 ++++++++++++++++++++++++++++++ shop/lib/actions/setup.ts | 124 +++++++++ 6 files changed, 560 insertions(+), 2 deletions(-) create mode 100644 shop/app/setup/page.tsx create mode 100644 shop/components/SetupWizard.tsx create mode 100644 shop/lib/actions/setup.ts diff --git a/shop/app/auth/login/page.tsx b/shop/app/auth/login/page.tsx index 5efe679..35cd447 100644 --- a/shop/app/auth/login/page.tsx +++ b/shop/app/auth/login/page.tsx @@ -1,6 +1,12 @@ 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 (
diff --git a/shop/app/page.tsx b/shop/app/page.tsx index 6bedfc0..36c2d43 100644 --- a/shop/app/page.tsx +++ b/shop/app/page.tsx @@ -1,9 +1,15 @@ import { createClient } from '@/lib/supabase/server' import { HomeClient } from '@/components/HomeClient' +import { isSetupNeeded } from '@/lib/actions/setup' +import { redirect } from 'next/navigation' export const dynamic = "force-dynamic" export default async function Home() { + if (await isSetupNeeded()) { + redirect('/setup') + } + let user = null try { const supabase = await createClient() diff --git a/shop/app/setup/page.tsx b/shop/app/setup/page.tsx new file mode 100644 index 0000000..70ec9b5 --- /dev/null +++ b/shop/app/setup/page.tsx @@ -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 +} diff --git a/shop/components/HeaderWrapper.tsx b/shop/components/HeaderWrapper.tsx index ad9dadc..b7010ff 100644 --- a/shop/components/HeaderWrapper.tsx +++ b/shop/components/HeaderWrapper.tsx @@ -11,8 +11,9 @@ interface HeaderWrapperProps { export function HeaderWrapper({ navbar, children }: HeaderWrapperProps) { const pathname = usePathname(); const isAdmin = pathname?.startsWith("/admin"); + const isSetup = pathname === "/setup"; - if (isAdmin) { + if (isAdmin || isSetup) { return <>{children}; } diff --git a/shop/components/SetupWizard.tsx b/shop/components/SetupWizard.tsx new file mode 100644 index 0000000..b256078 --- /dev/null +++ b/shop/components/SetupWizard.tsx @@ -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 ( +
+ {/* Dynamic Background Glow */} +
+
+ +
+ {/* Step Indicator Header */} +
+
+ CASPOS + Initialisierung +
+
+ {[1, 2, 3].map((s) => ( +
+ ))} +
+
+ + + {step === 1 && ( + +
+ +
+ +
+

Willkommen bei CASPOS!

+

+ Richten Sie Ihren persönlichen Lizenz- und Angebots-Shop in wenigen Schritten ein. Wir konfigurieren Ihr Administrator-Konto und die Mailverbindung. +

+
+ +
+
+ + Komplett self-hosted & sicher +
+
+ + Automatische Schema-Updates auf dem neuesten Stand +
+
+ + SMTP Mailversand für direkte Angebotsbestätigungen +
+
+ + +
+ )} + + {step === 2 && ( + +
+

+ + Admin-Konto anlegen +

+

+ Erstellen Sie den ersten Administrator-Benutzer. Dieser erhält vollen Zugriff auf das System. +

+
+ +
+
+
+ +
+ + setAdminForm({ ...adminForm, firstName: e.target.value })} + placeholder="Vorname" + className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600" + /> +
+
+
+ +
+ + setAdminForm({ ...adminForm, lastName: e.target.value })} + placeholder="Nachname" + className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600" + /> +
+
+
+ +
+ +
+ + 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" + /> +
+
+ +
+ +
+ + 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" + /> +
+
+ +
+
+ +
+ + setAdminForm({ ...adminForm, password: e.target.value })} + placeholder="••••••" + className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600" + /> +
+
+
+ +
+ + setAdminForm({ ...adminForm, confirmPassword: e.target.value })} + placeholder="••••••" + className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600" + /> +
+
+
+
+ +
+ + +
+
+ )} + + {step === 3 && ( + +
+

+ + SMTP-Mailserver konfigurieren +

+

+ Tragen Sie Ihre Mailserver-Daten ein, um automatisierte E-Mails an Partner und Kunden senden zu können. +

+
+ + {errorMsg && ( +
+ {errorMsg} +
+ )} + +
+
+
+ + setSmtpForm({ ...smtpForm, host: e.target.value })} + placeholder="smtp.domain.de" + className="bg-white/5 border-white/10 text-white placeholder:text-slate-600" + /> +
+
+ + setSmtpForm({ ...smtpForm, port: e.target.value })} + placeholder="587" + className="bg-white/5 border-white/10 text-white" + /> +
+
+ +
+ + setSmtpForm({ ...smtpForm, user: e.target.value })} + placeholder="mail@domain.de" + className="bg-white/5 border-white/10 text-white placeholder:text-slate-600" + /> +
+ +
+ + setSmtpForm({ ...smtpForm, pass: e.target.value })} + placeholder="••••••••••••" + className="bg-white/5 border-white/10 text-white placeholder:text-slate-600" + /> +
+ +
+ 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" + /> + +
+
+ +
+ + +
+
+ )} +
+
+
+ ) +} diff --git a/shop/lib/actions/setup.ts b/shop/lib/actions/setup.ts new file mode 100644 index 0000000..dd35b04 --- /dev/null +++ b/shop/lib/actions/setup.ts @@ -0,0 +1,124 @@ +'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 { + try { + const admin = createAdminClient() + const { count, error } = await admin + .from('users') + .select('*', { count: 'exact', head: true }) + + if (error) { + console.error('Error checking setup status:', error) + 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.' } + } +}