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
+
+
+
+ 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
+
+
+
+ )}
+
+ {step === 2 && (
+
+
+
+
+ Admin-Konto anlegen
+
+
+ Erstellen Sie den ersten Administrator-Benutzer. Dieser erhält vollen Zugriff auf das System.
+
+
+
+
+
+
+
Vorname *
+
+
+ setAdminForm({ ...adminForm, firstName: e.target.value })}
+ placeholder="Vorname"
+ className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
+ />
+
+
+
+
Nachname *
+
+
+ setAdminForm({ ...adminForm, lastName: e.target.value })}
+ placeholder="Nachname"
+ className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
+ />
+
+
+
+
+
+
Unternehmen / Partnername *
+
+
+ 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"
+ />
+
+
+
+
+
E-Mail-Adresse *
+
+
+ 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"
+ />
+
+
+
+
+
+
Passwort * (min. 6 Zeichen)
+
+
+ setAdminForm({ ...adminForm, password: e.target.value })}
+ placeholder="••••••"
+ className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
+ />
+
+
+
+
Passwort bestätigen *
+
+
+ setAdminForm({ ...adminForm, confirmPassword: e.target.value })}
+ placeholder="••••••"
+ className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
+ />
+
+
+
+
+
+
+
setStep(1)} className="text-white hover:bg-white/5">
+ Zurück
+
+
setStep(3)}
+ disabled={!isAdminFormValid}
+ className="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-bold"
+ >
+ Weiter zu SMTP
+
+
+
+
+ )}
+
+ {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}
+
+ )}
+
+
+
+
+ setStep(2)}
+ disabled={loading}
+ className="text-white hover:bg-white/5"
+ >
+ Zurück
+
+
+ {loading ? (
+ <>
+ Setup wird abgeschlossen...
+ >
+ ) : (
+ 'Einrichtung abschließen'
+ )}
+
+
+
+ )}
+
+
+
+ )
+}
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.' }
+ }
+}