diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ff8456a --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Email Configuration +RESEND_API_KEY=your_resend_api_key_here +EMAIL_FROM=noreply@yourdomain.com +ADMIN_EMAIL=admin@yourdomain.com + +# Admin Account Configuration +ADMIN_USERNAME=owner +ADMIN_PASSWORD_HASH=YWRtaW4xMjM= + +# OpenAI Configuration (optional) +OPENAI_API_KEY=your_openai_api_key_here + +# AWS Configuration (optional) +AWS_SECRET_ACCESS_KEY=your_aws_secret_key_here + +# Other API Keys (optional) +BW_CLIENTSECRET=your_bw_client_secret_here diff --git a/.gitignore b/.gitignore index 45c8b9d..f10f349 100644 --- a/.gitignore +++ b/.gitignore @@ -1,49 +1,49 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules/ -dist/ -dist-ssr/ -.vite/ -coverage/ - -# Editor directories and files -.idea -.DS_Store -Thumbs.db -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# Turbo -.turbo - -# Storage -*.local -.storage/ - -# Quests -.quests/ - -# SQLite -*.db-journal -*.db-wal -*.db-shm -etilqs_* - -# Environment variables -.env -.env.* -!.env.example -.env.local - -# TypeScript cache +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules/ +dist/ +dist-ssr/ +.vite/ +coverage/ + +# Editor directories and files +.idea +.DS_Store +Thumbs.db +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Turbo +.turbo + +# Storage +*.local +.storage/ + +# Quests +.quests/ + +# SQLite +*.db-journal +*.db-wal +*.db-shm +etilqs_* + +# Environment variables +.env +.env.* +!.env.example +.env.local + +# TypeScript cache *.tsbuildinfo \ No newline at end of file diff --git a/README.md b/README.md index 6a40e7e..dc46895 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Quests Base Template +# Stargirlnails Kiel - Nail Salon Booking System + +Ein vollständiges Buchungssystem für Nagelstudios mit Admin-Panel, Kalender und E-Mail-Benachrichtigungen. ## Dependencies @@ -9,3 +11,89 @@ - [oRPC](https://orpc.unnoq.com/) - [Hono](https://hono.dev/) - [Zod](https://zod.dev/) + +## Setup + +### 1. Umgebungsvariablen konfigurieren + +Kopiere die `.env.example` Datei zu `.env` und konfiguriere deine Umgebungsvariablen: + +```bash +cp .env.example .env +``` + +### 2. Admin-Passwort Hash generieren + +Das Admin-Passwort wird als Base64-Hash in der `.env` Datei gespeichert. Hier sind verschiedene Methoden, um einen Hash zu generieren: + +#### PowerShell (Windows) +```powershell +# Einfache Methode mit Base64-Encoding +$password = "dein_sicheres_passwort" +$bytes = [System.Text.Encoding]::UTF8.GetBytes($password) +$hash = [System.Convert]::ToBase64String($bytes) +Write-Host "Password Hash: $hash" + +# Alternative mit PowerShell 7+ (kürzer) +$password = "dein_sicheres_passwort" +[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($password)) +``` + +#### Node.js (falls verfügbar) +```javascript +// In der Node.js Konsole oder als separates Script +const password = "dein_sicheres_passwort"; +const hash = Buffer.from(password).toString('base64'); +console.log("Password Hash:", hash); +``` + +#### Online-Tools (nur für Entwicklung) +- Verwende einen Base64-Encoder wie [base64encode.org](https://www.base64encode.org/) + +### 3. .env Datei konfigurieren + +Bearbeite deine `.env` Datei und setze die generierten Werte: + +```env +# Admin Account Configuration +ADMIN_USERNAME=owner +ADMIN_PASSWORD_HASH=ZGVpbl9zaWNoZXJlc19wYXNzd29ydA== # Dein generierter Hash + +# Email Configuration +RESEND_API_KEY=your_resend_api_key_here +EMAIL_FROM=noreply@yourdomain.com +ADMIN_EMAIL=admin@yourdomain.com +``` + +### 4. Anwendung starten + +```bash +# Dependencies installieren +pnpm install + +# Entwicklungsserver starten +pnpm dev +``` + +## Features + +- 📅 **Terminbuchung**: Kunden können online Termine buchen +- 💅 **Behandlungsverwaltung**: Admin kann Behandlungen hinzufügen/bearbeiten +- 📆 **Kalender-Ansicht**: Übersichtliche Darstellung aller Termine +- ⏰ **Verfügbarkeits-Slots**: Flexible Slot-Verwaltung mit behandlungsspezifischen Dauern +- 📧 **E-Mail-Benachrichtigungen**: Automatische Benachrichtigungen bei Buchungen +- 🔐 **Admin-Panel**: Geschützter Bereich für Inhaber + +## Admin-Zugang + +Nach dem Setup kannst du dich mit den in der `.env` konfigurierten Admin-Credentials anmelden: +- **Benutzername**: Wert aus `ADMIN_USERNAME` +- **Passwort**: Das ursprüngliche Passwort (nicht der Hash) + +## Sicherheit + +⚠️ **Wichtige Hinweise:** +- Ändere das Standard-Passwort vor dem Produktionseinsatz +- Die aktuelle Passwort-Hashing-Methode (Base64) ist nur für Entwicklung geeignet +- Für Produktion sollte bcrypt oder ein ähnliches sicheres Hashing-Verfahren verwendet werden +- Speichere niemals echte Passwörter in der `.env` Datei, nur die Hashes diff --git a/scripts/start-with-email.ps1 b/scripts/start-with-email.ps1 index 906ff67..3f4f2db 100644 --- a/scripts/start-with-email.ps1 +++ b/scripts/start-with-email.ps1 @@ -1,20 +1,22 @@ $ErrorActionPreference = "Stop" -param( - [Parameter(Mandatory = $true)] - [string]$ResendApiKey, - - [Parameter(Mandatory = $false)] - [string]$EmailFrom = "Stargirlnails ", - - [Parameter(Mandatory = $false)] - [string]$AdminEmail -) - -Write-Host "Setting environment variables for Resend..." -$env:RESEND_API_KEY = $ResendApiKey -$env:EMAIL_FROM = $EmailFrom -if ($AdminEmail) { $env:ADMIN_EMAIL = $AdminEmail } +# Lade .env Datei automatisch +Write-Host "Loading .env file..." +if (Test-Path ".env") { + Get-Content ".env" | ForEach-Object { + if ($_ -match '^([^#][^=]+)=(.*)$') { + $name = $matches[1].Trim() + $value = $matches[2].Trim() + [Environment]::SetEnvironmentVariable($name, $value, 'Process') + Write-Host "✓ Loaded from .env: $name" -ForegroundColor Green + } + } + Write-Host "Environment variables loaded from .env file" -ForegroundColor Cyan +} else { + Write-Warning ".env file not found!" + Write-Host "Please create a .env file with your environment variables." -ForegroundColor Red + exit 1 +} Write-Host "Starting app with pnpm dev..." pnpm dev diff --git a/src/client/app.tsx b/src/client/app.tsx index 83d18fe..8761884 100644 --- a/src/client/app.tsx +++ b/src/client/app.tsx @@ -5,12 +5,13 @@ import { UserProfile } from "@/client/components/user-profile"; import { BookingForm } from "@/client/components/booking-form"; import { AdminTreatments } from "@/client/components/admin-treatments"; import { AdminBookings } from "@/client/components/admin-bookings"; +import { AdminCalendar } from "@/client/components/admin-calendar"; import { InitialDataLoader } from "@/client/components/initial-data-loader"; import { AdminAvailability } from "@/client/components/admin-availability"; function App() { const { user, isLoading, isOwner } = useAuth(); - const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings" | "admin-availability" | "profile">("booking"); + const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings" | "admin-calendar" | "admin-availability" | "profile">("booking"); // Show loading spinner while checking authentication if (isLoading) { @@ -29,7 +30,7 @@ function App() { } // Show login form if user is not authenticated and trying to access admin features - const needsAuth = !user && (activeTab === "admin-treatments" || activeTab === "admin-bookings" || activeTab === "admin-availability" || activeTab === "profile"); + const needsAuth = !user && (activeTab === "admin-treatments" || activeTab === "admin-bookings" || activeTab === "admin-calendar" || activeTab === "admin-availability" || activeTab === "profile"); if (needsAuth) { return ; } @@ -38,6 +39,7 @@ function App() { { id: "booking", label: "Termin buchen", icon: "📅", requiresAuth: false }, { id: "admin-treatments", label: "Behandlungen verwalten", icon: "💅", requiresAuth: true }, { id: "admin-bookings", label: "Buchungen verwalten", icon: "📋", requiresAuth: true }, + { id: "admin-calendar", label: "Kalender", icon: "📆", requiresAuth: true }, { id: "admin-availability", label: "Verfügbarkeiten", icon: "⏰", requiresAuth: true }, ...(user ? [{ id: "profile", label: "Profil", icon: "👤", requiresAuth: true }] : []), ] as const; @@ -169,6 +171,20 @@ function App() { )} + {activeTab === "admin-calendar" && isOwner && ( +
+
+

+ Kalender - Bevorstehende Buchungen +

+

+ Übersichtliche Darstellung aller bevorstehenden Termine im Kalenderformat. +

+
+ +
+ )} + {activeTab === "admin-availability" && isOwner && (
diff --git a/src/client/components/admin-availability.tsx b/src/client/components/admin-availability.tsx index ddec3c5..5730381 100644 --- a/src/client/components/admin-availability.tsx +++ b/src/client/components/admin-availability.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { queryClient } from "@/client/rpc-client"; @@ -7,14 +7,35 @@ export function AdminAvailability() { const [selectedDate, setSelectedDate] = useState(today); const [time, setTime] = useState("09:00"); const [duration, setDuration] = useState(30); + const [selectedTreatmentId, setSelectedTreatmentId] = useState(""); + const [slotType, setSlotType] = useState<"treatment" | "manual">("treatment"); const { data: allSlots } = useQuery( queryClient.availability.live.list.experimental_liveOptions() ); + const { data: treatments } = useQuery( + queryClient.treatments.live.list.experimental_liveOptions() + ); + const [errorMsg, setErrorMsg] = useState(""); const [successMsg, setSuccessMsg] = useState(""); + // Auto-clear messages after 5 seconds + useEffect(() => { + if (errorMsg) { + const timer = setTimeout(() => setErrorMsg(""), 5000); + return () => clearTimeout(timer); + } + }, [errorMsg]); + + useEffect(() => { + if (successMsg) { + const timer = setTimeout(() => setSuccessMsg(""), 5000); + return () => clearTimeout(timer); + } + }, [successMsg]); + const { mutate: createSlot, isPending: isCreating } = useMutation( queryClient.availability.create.mutationOptions() ); @@ -22,30 +43,60 @@ export function AdminAvailability() { queryClient.availability.remove.mutationOptions() ); + // Auto-update duration when treatment is selected + useEffect(() => { + if (selectedTreatmentId && treatments) { + const treatment = treatments.find(t => t.id === selectedTreatmentId); + if (treatment) { + setDuration(treatment.duration); + } + } + }, [selectedTreatmentId, treatments]); + + // Get selected treatment details + const selectedTreatment = treatments?.find(t => t.id === selectedTreatmentId); + + // Get treatment name for display + const getTreatmentName = (treatmentId: string) => { + return treatments?.find(t => t.id === treatmentId)?.name || "Unbekannte Behandlung"; + }; + const addSlot = () => { setErrorMsg(""); setSuccessMsg(""); + + // Validation based on slot type + if (slotType === "treatment" && !selectedTreatmentId) { + setErrorMsg("Bitte eine Behandlung auswählen."); + return; + } if (!selectedDate || !time || !duration) { setErrorMsg("Bitte Datum, Uhrzeit und Dauer angeben."); return; } + const sessionId = localStorage.getItem("sessionId") || ""; if (!sessionId) { setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden."); return; } + createSlot( { sessionId, date: selectedDate, time, durationMinutes: duration }, { onSuccess: () => { - setSuccessMsg("Slot angelegt."); - // advance time to next 30-minute step + const slotDescription = slotType === "treatment" + ? `${getTreatmentName(selectedTreatmentId)} (${duration} Min)` + : `Manueller Slot (${duration} Min)`; + setSuccessMsg(`${slotDescription} angelegt.`); + + // advance time by the duration of the slot const [hStr, mStr] = time.split(":"); let h = parseInt(hStr, 10); let m = parseInt(mStr, 10); - m += 30; - if (m >= 60) { h += 1; m -= 60; } - if (h >= 24) { h = 0; } + m += duration; + if (m >= 60) { h += Math.floor(m / 60); m = m % 60; } + if (h >= 24) { h = h % 24; } const next = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; setTime(next); }, @@ -58,76 +109,274 @@ export function AdminAvailability() { }; return ( -
-

Verfügbarkeiten verwalten

+
+

Verfügbarkeiten verwalten

-
- setSelectedDate(e.target.value)} - className="border rounded px-3 py-2" - /> - setTime(e.target.value)} - className="border rounded px-3 py-2" - /> - setDuration(Number(e.target.value))} - className="border rounded px-3 py-2 w-28" - /> - + {/* Slot Type Selection */} +
+

Neuen Slot anlegen

+ +
+ + +
+ +
+
+ + setSelectedDate(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500" + /> +
+ +
+ + setTime(e.target.value)} + className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500" + /> +
+ + {slotType === "treatment" ? ( +
+ + +
+ ) : ( +
+ + setDuration(Number(e.target.value))} + className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500" + /> +
+ )} + +
+ +
+
+ + {/* Treatment Info Display */} + {slotType === "treatment" && selectedTreatment && ( +
+
+
+

{selectedTreatment.name}

+

{selectedTreatment.description}

+
+
+
+ {(selectedTreatment.price / 100).toFixed(2)} € +
+
+ {selectedTreatment.duration} Minuten +
+
+
+
+ )}
{(errorMsg || successMsg) && ( -
- {errorMsg &&
{errorMsg}
} - {successMsg &&
{successMsg}
} +
+ {errorMsg && ( +
+
+ + + + Fehler: + {errorMsg} +
+
+ )} + {successMsg && ( +
+
+ + + + Erfolg: + {successMsg} +
+
+ )}
)} -
-

Alle freien Slots

-
+ {/* Quick Stats */} +
+
+
+ {allSlots?.filter(s => s.status === "free").length || 0} +
+
Freie Slots
+
+
+
+ {allSlots?.filter(s => s.status === "reserved").length || 0} +
+
Reservierte Slots
+
+
+
+ {allSlots?.length || 0} +
+
Slots gesamt
+
+
+ + {/* All Slots List */} +
+
+

Alle Slots

+
+
{allSlots - ?.filter((s) => s.status === "free") - .sort((a, b) => (a.date === b.date ? a.time.localeCompare(b.time) : a.date.localeCompare(b.date))) - .map((slot) => ( -
-
- {slot.date} - {slot.time} - {slot.durationMinutes} Min - - {slot.status === "free" ? "frei" : "reserviert"} - + ?.sort((a, b) => (a.date === b.date ? a.time.localeCompare(b.time) : a.date.localeCompare(b.date))) + .map((slot) => { + // Try to find matching treatment based on duration + const matchingTreatments = treatments?.filter(t => t.duration === slot.durationMinutes) || []; + + return ( +
+
+
+
+
Datum
+
{new Date(slot.date).toLocaleDateString('de-DE')}
+
+
+
Zeit
+
{slot.time}
+
+
+
Dauer
+
{slot.durationMinutes} Min
+
+ {matchingTreatments.length > 0 && ( +
+
Passende Behandlungen
+
+ {matchingTreatments.length === 1 + ? matchingTreatments[0].name + : `${matchingTreatments.length} Behandlungen` + } +
+
+ )} + + {slot.status === "free" ? "Frei" : "Reserviert"} + +
+
+ +
+
+ + {/* Show matching treatments if multiple */} + {matchingTreatments.length > 1 && ( +
+
Passende Behandlungen:
+
+ {matchingTreatments.map(treatment => ( + + {treatment.name} + + ))} +
+
+ )}
-
- -
-
- ))} - {allSlots?.filter((s) => s.status === "free").length === 0 && ( -
Keine freien Slots vorhanden.
+ ); + })} + {allSlots?.length === 0 && ( +
+
Keine Slots vorhanden
+
Legen Sie den ersten Slot an, um zu beginnen.
+
)}
diff --git a/src/client/components/admin-calendar.tsx b/src/client/components/admin-calendar.tsx new file mode 100644 index 0000000..ab261d6 --- /dev/null +++ b/src/client/components/admin-calendar.tsx @@ -0,0 +1,322 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { queryClient } from "@/client/rpc-client"; + +export function AdminCalendar() { + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(null); + + const { data: bookings } = useQuery( + queryClient.bookings.live.list.experimental_liveOptions() + ); + + const { data: treatments } = useQuery( + queryClient.treatments.live.list.experimental_liveOptions() + ); + + const { mutate: updateBookingStatus } = useMutation( + queryClient.bookings.updateStatus.mutationOptions() + ); + + const getTreatmentName = (treatmentId: string) => { + return treatments?.find(t => t.id === treatmentId)?.name || "Unbekannte Behandlung"; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "pending": return "bg-yellow-100 text-yellow-800 border-yellow-200"; + case "confirmed": return "bg-green-100 text-green-800 border-green-200"; + case "cancelled": return "bg-red-100 text-red-800 border-red-200"; + case "completed": return "bg-blue-100 text-blue-800 border-blue-200"; + default: return "bg-gray-100 text-gray-800 border-gray-200"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "pending": return "Ausstehend"; + case "confirmed": return "Bestätigt"; + case "cancelled": return "Storniert"; + case "completed": return "Abgeschlossen"; + default: return status; + } + }; + + // Filter upcoming bookings (today and future, not cancelled) + const upcomingBookings = bookings?.filter(booking => { + const bookingDate = new Date(booking.appointmentDate); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return bookingDate >= today && booking.status !== "cancelled"; + }).sort((a, b) => { + if (a.appointmentDate === b.appointmentDate) { + return a.appointmentTime.localeCompare(b.appointmentTime); + } + return a.appointmentDate.localeCompare(b.appointmentDate); + }); + + // Get bookings for a specific date + const getBookingsForDate = (date: string) => { + return upcomingBookings?.filter(booking => booking.appointmentDate === date) || []; + }; + + // Calendar generation + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const startDate = new Date(firstDay); + startDate.setDate(startDate.getDate() - firstDay.getDay()); + + const calendarDays = []; + const currentDate = new Date(startDate); + + for (let i = 0; i < 42; i++) { + calendarDays.push(new Date(currentDate)); + currentDate.setDate(currentDate.getDate() + 1); + } + + const monthNames = [ + "Januar", "Februar", "März", "April", "Mai", "Juni", + "Juli", "August", "September", "Oktober", "November", "Dezember" + ]; + + const dayNames = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; + + const navigateMonth = (direction: 'prev' | 'next') => { + const newMonth = new Date(currentMonth); + if (direction === 'prev') { + newMonth.setMonth(month - 1); + } else { + newMonth.setMonth(month + 1); + } + setCurrentMonth(newMonth); + setSelectedDate(null); + }; + + const handleStatusUpdate = (bookingId: string, newStatus: string) => { + const sessionId = localStorage.getItem('sessionId'); + if (!sessionId) return; + + updateBookingStatus({ + sessionId, + id: bookingId, + status: newStatus as "pending" | "confirmed" | "cancelled" | "completed" + }); + }; + + const today = new Date().toISOString().split('T')[0]; + + return ( +
+

Kalender - Bevorstehende Buchungen

+ + {/* Quick Stats */} +
+
+
+ {upcomingBookings?.filter(b => b.status === "pending").length || 0} +
+
Ausstehende Bestätigungen
+
+
+
+ {upcomingBookings?.filter(b => b.status === "confirmed").length || 0} +
+
Bestätigte Termine
+
+
+
+ {upcomingBookings?.filter(b => b.appointmentDate === today).length || 0} +
+
Heute
+
+
+
+ {upcomingBookings?.length || 0} +
+
Bevorstehende Termine
+
+
+ + {/* Calendar */} +
+ {/* Calendar Header */} +
+ + +

+ {monthNames[month]} {year} +

+ + +
+ + {/* Calendar Grid */} +
+ {/* Day headers */} + {dayNames.map(day => ( +
+ {day} +
+ ))} + + {/* Calendar days */} + {calendarDays.map((date, index) => { + const dateStr = date.toISOString().split('T')[0]; + const isCurrentMonth = date.getMonth() === month; + const isToday = dateStr === today; + const isSelected = selectedDate === dateStr; + const dayBookings = getBookingsForDate(dateStr); + + return ( +
setSelectedDate(isSelected ? null : dateStr)} + > +
+ {date.getDate()} +
+ + {/* Show bookings for this day */} +
+ {dayBookings.slice(0, 2).map(booking => ( +
+
{booking.appointmentTime}
+
{booking.customerName}
+
+ ))} + {dayBookings.length > 2 && ( +
+ +{dayBookings.length - 2} weitere +
+ )} +
+
+ ); + })} +
+
+ + {/* Selected Date Details */} + {selectedDate && ( +
+
+

+ Termine für {new Date(selectedDate).toLocaleDateString('de-DE', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} +

+ +
+ + {getBookingsForDate(selectedDate).length === 0 ? ( +

Keine Termine für diesen Tag

+ ) : ( +
+ {getBookingsForDate(selectedDate).map(booking => ( +
+
+
+
+

{booking.customerName}

+ + {getStatusText(booking.status)} + +
+ +
+
+ Behandlung: {getTreatmentName(booking.treatmentId)} +
+
+ Uhrzeit: {booking.appointmentTime} +
+
+ E-Mail: {booking.customerEmail} +
+
+ Telefon: {booking.customerPhone} +
+
+ + {booking.notes && ( +
+ Notizen: +

{booking.notes}

+
+ )} +
+ + {/* Status Update Buttons */} +
+ {booking.status === "pending" && ( + <> + + + + )} + + {booking.status === "confirmed" && ( + + )} +
+
+
+ ))} +
+ )} +
+ )} +
+ ); +} diff --git a/src/server/rpc/auth.ts b/src/server/rpc/auth.ts index a78a3fa..f96644e 100644 --- a/src/server/rpc/auth.ts +++ b/src/server/rpc/auth.ts @@ -34,20 +34,31 @@ const verifyPassword = (password: string, hash: string): boolean => { return hashPassword(password) === hash; }; +// Export hashPassword for external use (e.g., generating hashes for .env) +export const generatePasswordHash = hashPassword; + // Initialize default owner account const initializeOwner = async () => { const existingUsers = await usersKV.getAllItems(); if (existingUsers.length === 0) { const ownerId = randomUUID(); + + // Get admin credentials from environment variables + const adminUsername = process.env.ADMIN_USERNAME || "owner"; + const adminPasswordHash = process.env.ADMIN_PASSWORD_HASH || hashPassword("admin123"); + const adminEmail = process.env.ADMIN_EMAIL || "owner@stargirlnails.de"; + const owner: User = { id: ownerId, - username: "owner", - email: "owner@stargirlnails.de", - passwordHash: hashPassword("admin123"), // Default password + username: adminUsername, + email: adminEmail, + passwordHash: adminPasswordHash, role: "owner", createdAt: new Date().toISOString(), }; await usersKV.setItem(ownerId, owner); + + console.log(`✅ Admin account created: username="${adminUsername}", email="${adminEmail}"`); } };