Füge eine Benutzerverwaltung hinzu, damit "Manage Treatments" und "Manage Bookings" nur für den Shop Inhaber zugänglich ist.

This commit is contained in:
Quests Agent
2025-09-29 18:11:20 +02:00
parent e999180732
commit 11d17213c1
8 changed files with 661 additions and 53 deletions

View File

@@ -1,16 +1,39 @@
import { useState } from "react"; import { useState } from "react";
import { useAuth } from "@/client/components/auth-provider";
import { LoginForm } from "@/client/components/login-form";
import { UserProfile } from "@/client/components/user-profile";
import { BookingForm } from "@/client/components/booking-form"; import { BookingForm } from "@/client/components/booking-form";
import { AdminTreatments } from "@/client/components/admin-treatments"; import { AdminTreatments } from "@/client/components/admin-treatments";
import { AdminBookings } from "@/client/components/admin-bookings"; import { AdminBookings } from "@/client/components/admin-bookings";
import { InitialDataLoader } from "@/client/components/initial-data-loader"; import { InitialDataLoader } from "@/client/components/initial-data-loader";
function App() { function App() {
const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings">("booking"); const { user, isLoading, isOwner } = useAuth();
const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings" | "profile">("booking");
// Show loading spinner while checking authentication
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-pink-50 to-purple-50">
<div className="text-center">
<div className="text-6xl mb-4">💅</div>
<div className="text-lg text-gray-600">Lade...</div>
</div>
</div>
);
}
// Show login form if user is not authenticated and trying to access admin features
const needsAuth = !user && (activeTab === "admin-treatments" || activeTab === "admin-bookings" || activeTab === "profile");
if (needsAuth) {
return <LoginForm />;
}
const tabs = [ const tabs = [
{ id: "booking", label: "Book Appointment", icon: "📅" }, { id: "booking", label: "Termin buchen", icon: "📅", requiresAuth: false },
{ id: "admin-treatments", label: "Manage Treatments", icon: "💅" }, { id: "admin-treatments", label: "Behandlungen verwalten", icon: "💅", requiresAuth: true },
{ id: "admin-bookings", label: "Manage Bookings", icon: "📋" }, { id: "admin-bookings", label: "Buchungen verwalten", icon: "📋", requiresAuth: true },
...(user ? [{ id: "profile", label: "Profil", icon: "👤", requiresAuth: true }] : []),
] as const; ] as const;
return ( return (
@@ -28,6 +51,19 @@ function App() {
<p className="text-sm text-gray-600">Professional Nail Design & Care</p> <p className="text-sm text-gray-600">Professional Nail Design & Care</p>
</div> </div>
</div> </div>
{user && (
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600">
Willkommen, {user.username}
</span>
{isOwner && (
<span className="bg-pink-100 text-pink-800 px-2 py-1 rounded-full text-xs font-medium">
Inhaber
</span>
)}
</div>
)}
</div> </div>
</div> </div>
</header> </header>
@@ -36,20 +72,44 @@ function App() {
<nav className="bg-white shadow-sm"> <nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex space-x-8"> <div className="flex space-x-8">
{tabs.map((tab) => ( {tabs.map((tab) => {
// Hide admin tabs for non-owners
if (tab.requiresAuth && !isOwner && tab.id !== "profile") {
return null;
}
return (
<button
key={tab.id}
onClick={() => {
if (tab.requiresAuth && !user) {
// This will trigger the login form
setActiveTab(tab.id as any);
} else {
setActiveTab(tab.id as any);
}
}}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
activeTab === tab.id
? "border-pink-500 text-pink-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<span>{tab.icon}</span>
<span>{tab.label}</span>
</button>
);
})}
{!user && (
<button <button
key={tab.id} onClick={() => setActiveTab("profile")} // This will trigger login
onClick={() => setActiveTab(tab.id)} className="py-4 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 font-medium text-sm flex items-center space-x-2"
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
activeTab === tab.id
? "border-pink-500 text-pink-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
> >
<span>{tab.icon}</span> <span>🔑</span>
<span>{tab.label}</span> <span>Inhaber Login</span>
</button> </button>
))} )}
</div> </div>
</div> </div>
</nav> </nav>
@@ -60,44 +120,58 @@ function App() {
<div> <div>
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2 className="text-3xl font-bold text-gray-900 mb-4">
Book Your Perfect Nail Treatment Buchen Sie Ihre perfekte Nagelbehandlung
</h2> </h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto"> <p className="text-lg text-gray-600 max-w-2xl mx-auto">
Experience professional nail care with our expert technicians. Erleben Sie professionelle Nagelpflege mit unseren Experten.
Choose from our wide range of treatments and book your appointment today. Wählen Sie aus unserem breiten Angebot an Behandlungen und buchen Sie noch heute Ihren Termin.
</p> </p>
</div> </div>
<BookingForm /> <BookingForm />
</div> </div>
)} )}
{activeTab === "admin-treatments" && ( {activeTab === "admin-treatments" && isOwner && (
<div> <div>
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2 className="text-3xl font-bold text-gray-900 mb-4">
Treatment Management Behandlungen verwalten
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600">
Add, edit, and manage your nail treatment services. Hinzufügen, bearbeiten und verwalten Sie Ihre Nagelbehandlungen.
</p> </p>
</div> </div>
<AdminTreatments /> <AdminTreatments />
</div> </div>
)} )}
{activeTab === "admin-bookings" && ( {activeTab === "admin-bookings" && isOwner && (
<div> <div>
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2 className="text-3xl font-bold text-gray-900 mb-4">
Booking Management Buchungen verwalten
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600">
View and manage customer appointments and bookings. Sehen und verwalten Sie Kundentermine und Buchungen.
</p> </p>
</div> </div>
<AdminBookings /> <AdminBookings />
</div> </div>
)} )}
{activeTab === "profile" && user && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Benutzerprofil
</h2>
<p className="text-lg text-gray-600">
Verwalten Sie Ihre Kontoinformationen und Einstellungen.
</p>
</div>
<UserProfile />
</div>
)}
</main> </main>
{/* Footer */} {/* Footer */}

View File

@@ -0,0 +1,114 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
import { useMutation } from "@tanstack/react-query";
import { queryClient } from "@/client/rpc-client";
interface User {
id: string;
username: string;
email: string;
role: "customer" | "owner";
}
interface AuthContextType {
user: User | null;
sessionId: string | null;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
isOwner: boolean;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [sessionId, setSessionId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { mutateAsync: loginMutation } = useMutation(
queryClient.auth.login.mutationOptions()
);
const { mutateAsync: logoutMutation } = useMutation(
queryClient.auth.logout.mutationOptions()
);
const { mutateAsync: verifySessionMutation } = useMutation(
queryClient.auth.verifySession.mutationOptions()
);
useEffect(() => {
// Check for existing session on app load
const storedSessionId = localStorage.getItem("sessionId");
if (storedSessionId) {
verifySessionMutation(storedSessionId)
.then((result) => {
setUser(result.user);
setSessionId(storedSessionId);
})
.catch(() => {
localStorage.removeItem("sessionId");
})
.finally(() => {
setIsLoading(false);
});
} else {
setIsLoading(false);
}
}, [verifySessionMutation]);
const login = async (username: string, password: string) => {
try {
const result = await loginMutation({ username, password });
setUser(result.user);
setSessionId(result.sessionId);
localStorage.setItem("sessionId", result.sessionId);
} catch (error) {
throw error;
}
};
const logout = async () => {
if (sessionId) {
try {
await logoutMutation(sessionId);
} catch (error) {
// Continue with logout even if server call fails
console.error("Logout error:", error);
}
}
setUser(null);
setSessionId(null);
localStorage.removeItem("sessionId");
};
const isOwner = user?.role === "owner";
const value: AuthContextType = {
user,
sessionId,
isLoading,
login,
logout,
isOwner,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}

View File

@@ -64,7 +64,7 @@ export function BookingForm() {
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !appointmentTime) { if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !appointmentTime) {
alert("Please fill in all required fields"); alert("Bitte füllen Sie alle erforderlichen Felder aus");
return; return;
} }
@@ -85,7 +85,7 @@ export function BookingForm() {
setAppointmentDate(""); setAppointmentDate("");
setAppointmentTime(""); setAppointmentTime("");
setNotes(""); setNotes("");
alert("Booking created successfully! We'll contact you to confirm your appointment."); alert("Buchung erfolgreich erstellt! Wir werden Sie kontaktieren, um Ihren Termin zu bestätigen.");
} }
}); });
}; };
@@ -95,13 +95,13 @@ export function BookingForm() {
return ( return (
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6"> <div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Book Your Nail Treatment</h2> <h2 className="text-2xl font-bold text-gray-900 mb-6">Buchen Sie Ihre Nagelbehandlung</h2>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Treatment Selection */} {/* Treatment Selection */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Select Treatment * Behandlung auswählen *
</label> </label>
<select <select
value={selectedTreatment} value={selectedTreatment}
@@ -109,7 +109,7 @@ export function BookingForm() {
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required required
> >
<option value="">Choose a treatment</option> <option value="">Wählen Sie eine Behandlung</option>
{treatments?.map((treatment) => ( {treatments?.map((treatment) => (
<option key={treatment.id} value={treatment.id}> <option key={treatment.id} value={treatment.id}>
{treatment.name} - ${(treatment.price / 100).toFixed(2)} ({treatment.duration} min) {treatment.name} - ${(treatment.price / 100).toFixed(2)} ({treatment.duration} min)
@@ -125,7 +125,7 @@ export function BookingForm() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Full Name * Vollständiger Name *
</label> </label>
<input <input
type="text" type="text"
@@ -137,7 +137,7 @@ export function BookingForm() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Email * E-Mail *
</label> </label>
<input <input
type="email" type="email"
@@ -151,7 +151,7 @@ export function BookingForm() {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Phone Number * Telefonnummer *
</label> </label>
<input <input
type="tel" type="tel"
@@ -166,7 +166,7 @@ export function BookingForm() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Preferred Date * Gewünschtes Datum *
</label> </label>
<input <input
type="date" type="date"
@@ -179,7 +179,7 @@ export function BookingForm() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Preferred Time * Gewünschte Uhrzeit *
</label> </label>
<select <select
value={appointmentTime} value={appointmentTime}
@@ -188,7 +188,7 @@ export function BookingForm() {
disabled={!appointmentDate || !selectedTreatment} disabled={!appointmentDate || !selectedTreatment}
required required
> >
<option value="">Select time</option> <option value="">Zeit auswählen</option>
{availableTimeSlots.map((time) => ( {availableTimeSlots.map((time) => (
<option key={time} value={time}> <option key={time} value={time}>
{time} {time}
@@ -201,14 +201,14 @@ export function BookingForm() {
{/* Notes */} {/* Notes */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Additional Notes Zusätzliche Notizen
</label> </label>
<textarea <textarea
value={notes} value={notes}
onChange={(e) => setNotes(e.target.value)} onChange={(e) => setNotes(e.target.value)}
rows={3} rows={3}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
placeholder="Any special requests or information..." placeholder="Besondere Wünsche oder Informationen..."
/> />
</div> </div>
@@ -217,7 +217,7 @@ export function BookingForm() {
disabled={isPending} disabled={isPending}
className="w-full bg-pink-600 text-white py-3 px-4 rounded-md hover:bg-pink-700 focus:ring-2 focus:ring-pink-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed font-medium" className="w-full bg-pink-600 text-white py-3 px-4 rounded-md hover:bg-pink-700 focus:ring-2 focus:ring-pink-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
> >
{isPending ? "Booking..." : "Book Appointment"} {isPending ? "Wird gebucht..." : "Termin buchen"}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -0,0 +1,85 @@
import { useState } from "react";
import { useAuth } from "@/client/components/auth-provider";
export function LoginForm() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setIsLoading(true);
try {
await login(username, password);
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed");
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-pink-50 to-purple-50">
<div className="max-w-md w-full space-y-8 p-8">
<div className="text-center">
<div className="text-6xl mb-4">💅</div>
<h2 className="text-3xl font-bold text-gray-900">Stargirlnails Kiel</h2>
<p className="mt-2 text-sm text-gray-600">Inhaber Login</p>
</div>
<form className="mt-8 space-y-6 bg-white p-6 rounded-lg shadow-lg" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Benutzername
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Passwort
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-pink-600 text-white py-3 px-4 rounded-md hover:bg-pink-700 focus:ring-2 focus:ring-pink-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{isLoading ? "Anmelden..." : "Anmelden"}
</button>
</form>
<div className="text-center text-sm text-gray-600">
<p>Standard Login:</p>
<p className="font-mono">Benutzername: owner</p>
<p className="font-mono">Passwort: admin123</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,165 @@
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { useAuth } from "@/client/components/auth-provider";
import { queryClient } from "@/client/rpc-client";
export function UserProfile() {
const { user, sessionId, logout } = useAuth();
const [showPasswordChange, setShowPasswordChange] = useState(false);
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const { mutate: changePassword, isPending } = useMutation(
queryClient.auth.changePassword.mutationOptions()
);
const handlePasswordChange = (e: React.FormEvent) => {
e.preventDefault();
setError("");
setMessage("");
if (newPassword !== confirmPassword) {
setError("Neue Passwörter stimmen nicht überein");
return;
}
if (newPassword.length < 6) {
setError("Neues Passwort muss mindestens 6 Zeichen lang sein");
return;
}
if (!sessionId) {
setError("Keine gültige Sitzung");
return;
}
changePassword({
sessionId,
currentPassword,
newPassword,
}, {
onSuccess: () => {
setMessage("Passwort erfolgreich geändert");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
setShowPasswordChange(false);
},
onError: (err) => {
setError(err instanceof Error ? err.message : "Fehler beim Ändern des Passworts");
}
});
};
if (!user) return null;
return (
<div className="bg-white rounded-lg shadow-lg p-6 max-w-2xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">Benutzerprofil</h2>
<button
onClick={logout}
className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 font-medium"
>
Abmelden
</button>
</div>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-gray-700">Benutzername</label>
<p className="mt-1 text-lg text-gray-900">{user.username}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">E-Mail</label>
<p className="mt-1 text-lg text-gray-900">{user.email}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Rolle</label>
<p className="mt-1 text-lg text-gray-900 capitalize">{user.role === "owner" ? "Inhaber" : "Kunde"}</p>
</div>
</div>
<div>
<button
onClick={() => setShowPasswordChange(!showPasswordChange)}
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 font-medium"
>
{showPasswordChange ? "Abbrechen" : "Passwort ändern"}
</button>
</div>
{showPasswordChange && (
<form onSubmit={handlePasswordChange} className="mt-6 space-y-4 border-t pt-6">
<h3 className="text-lg font-semibold text-gray-900">Passwort ändern</h3>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
{message && (
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
{message}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Aktuelles Passwort
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Neues Passwort
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
minLength={6}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Neues Passwort bestätigen
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
minLength={6}
required
/>
</div>
<button
type="submit"
disabled={isPending}
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 disabled:opacity-50 font-medium"
>
{isPending ? "Ändern..." : "Passwort ändern"}
</button>
</form>
)}
</div>
);
}

View File

@@ -1,15 +1,18 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "./styles/globals.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./app.tsx"; import { AuthProvider } from "@/client/components/auth-provider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import App from "./app.tsx";
import "./styles/globals.css";
const queryClient = new QueryClient();
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<StrictMode> createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}> <StrictMode>
<App /> <QueryClientProvider client={queryClient}>
</QueryClientProvider> <AuthProvider>
</StrictMode>, <App />
); </AuthProvider>
</QueryClientProvider>
</StrictMode>
);

165
src/server/rpc/auth.ts Normal file
View File

@@ -0,0 +1,165 @@
import { call, os } from "@orpc/server";
import { z } from "zod";
import { randomUUID } from "crypto";
import { createKV } from "@/server/lib/create-kv";
const UserSchema = z.object({
id: z.string(),
username: z.string(),
email: z.string(),
passwordHash: z.string(),
role: z.enum(["customer", "owner"]),
createdAt: z.string(),
});
const SessionSchema = z.object({
id: z.string(),
userId: z.string(),
expiresAt: z.string(),
createdAt: z.string(),
});
type User = z.output<typeof UserSchema>;
type Session = z.output<typeof SessionSchema>;
const usersKV = createKV<User>("users");
const sessionsKV = createKV<Session>("sessions");
// Simple password hashing (in production, use bcrypt or similar)
const hashPassword = (password: string): string => {
return Buffer.from(password).toString('base64');
};
const verifyPassword = (password: string, hash: string): boolean => {
return hashPassword(password) === hash;
};
// Initialize default owner account
const initializeOwner = async () => {
const existingUsers = await usersKV.getAllItems();
if (existingUsers.length === 0) {
const ownerId = randomUUID();
const owner: User = {
id: ownerId,
username: "owner",
email: "owner@stargirlnails.de",
passwordHash: hashPassword("admin123"), // Default password
role: "owner",
createdAt: new Date().toISOString(),
};
await usersKV.setItem(ownerId, owner);
}
};
// Initialize on module load
initializeOwner();
const login = os
.input(z.object({
username: z.string(),
password: z.string(),
}))
.handler(async ({ input }) => {
const users = await usersKV.getAllItems();
const user = users.find(u => u.username === input.username);
if (!user || !verifyPassword(input.password, user.passwordHash)) {
throw new Error("Invalid credentials");
}
// Create session
const sessionId = randomUUID();
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24); // 24 hours
const session: Session = {
id: sessionId,
userId: user.id,
expiresAt: expiresAt.toISOString(),
createdAt: new Date().toISOString(),
};
await sessionsKV.setItem(sessionId, session);
return {
sessionId,
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
},
};
});
const logout = os
.input(z.string()) // sessionId
.handler(async ({ input }) => {
await sessionsKV.removeItem(input);
return { success: true };
});
const verifySession = os
.input(z.string()) // sessionId
.handler(async ({ input }) => {
const session = await sessionsKV.getItem(input);
if (!session) {
throw new Error("Invalid session");
}
if (new Date(session.expiresAt) < new Date()) {
await sessionsKV.removeItem(input);
throw new Error("Session expired");
}
const user = await usersKV.getItem(session.userId);
if (!user) {
throw new Error("User not found");
}
return {
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
},
};
});
const changePassword = os
.input(z.object({
sessionId: z.string(),
currentPassword: z.string(),
newPassword: z.string(),
}))
.handler(async ({ input }) => {
const session = await sessionsKV.getItem(input.sessionId);
if (!session) {
throw new Error("Invalid session");
}
const user = await usersKV.getItem(session.userId);
if (!user) {
throw new Error("User not found");
}
if (!verifyPassword(input.currentPassword, user.passwordHash)) {
throw new Error("Current password is incorrect");
}
const updatedUser = {
...user,
passwordHash: hashPassword(input.newPassword),
};
await usersKV.setItem(user.id, updatedUser);
return { success: true };
});
export const router = {
login,
logout,
verifySession,
changePassword,
};

View File

@@ -1,9 +1,11 @@
import { demo } from "./demo"; import { demo } from "./demo";
import { router as treatments } from "./treatments"; import { router as treatments } from "./treatments";
import { router as bookings } from "./bookings"; import { router as bookings } from "./bookings";
import { router as auth } from "./auth";
export const router = { export const router = {
demo, demo,
treatments, treatments,
bookings, bookings,
auth,
}; };