- Preferred Date *
+ GewĂĽnschtes Datum *
- Preferred Time *
+ GewĂĽnschte Uhrzeit *
- Select time
+ Zeit auswählen
{availableTimeSlots.map((time) => (
{time}
@@ -201,14 +201,14 @@ export function BookingForm() {
{/* Notes */}
- Additional Notes
+ Zusätzliche Notizen
@@ -217,7 +217,7 @@ export function BookingForm() {
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"
>
- {isPending ? "Booking..." : "Book Appointment"}
+ {isPending ? "Wird gebucht..." : "Termin buchen"}
diff --git a/src/client/components/login-form.tsx b/src/client/components/login-form.tsx
new file mode 100644
index 0000000..477e743
--- /dev/null
+++ b/src/client/components/login-form.tsx
@@ -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 (
+
+
+
+
đź’…
+
Stargirlnails Kiel
+
Inhaber Login
+
+
+
+
+
+
Standard Login:
+
Benutzername: owner
+
Passwort: admin123
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/client/components/user-profile.tsx b/src/client/components/user-profile.tsx
new file mode 100644
index 0000000..5337d4f
--- /dev/null
+++ b/src/client/components/user-profile.tsx
@@ -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 (
+
+
+
Benutzerprofil
+
+ Abmelden
+
+
+
+
+
+
Benutzername
+
{user.username}
+
+
+
+
E-Mail
+
{user.email}
+
+
+
+
Rolle
+
{user.role === "owner" ? "Inhaber" : "Kunde"}
+
+
+
+
+ setShowPasswordChange(!showPasswordChange)}
+ className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 font-medium"
+ >
+ {showPasswordChange ? "Abbrechen" : "Passwort ändern"}
+
+
+
+ {showPasswordChange && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/client/main.tsx b/src/client/main.tsx
index 172dba0..73e8e66 100644
--- a/src/client/main.tsx
+++ b/src/client/main.tsx
@@ -1,15 +1,18 @@
-import { StrictMode } from "react";
-import { createRoot } from "react-dom/client";
-import "./styles/globals.css";
-import App from "./app.tsx";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-
-const queryClient = new QueryClient();
-
-createRoot(document.getElementById("root")!).render(
-
-
-
-
- ,
-);
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { AuthProvider } from "@/client/components/auth-provider";
+import App from "./app.tsx";
+import "./styles/globals.css";
+
+const queryClient = new QueryClient();
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+
+
+);
diff --git a/src/server/rpc/auth.ts b/src/server/rpc/auth.ts
new file mode 100644
index 0000000..a78a3fa
--- /dev/null
+++ b/src/server/rpc/auth.ts
@@ -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
;
+type Session = z.output;
+
+const usersKV = createKV("users");
+const sessionsKV = createKV("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,
+};
\ No newline at end of file
diff --git a/src/server/rpc/index.ts b/src/server/rpc/index.ts
index 39712a5..8f55d27 100644
--- a/src/server/rpc/index.ts
+++ b/src/server/rpc/index.ts
@@ -1,9 +1,11 @@
import { demo } from "./demo";
import { router as treatments } from "./treatments";
import { router as bookings } from "./bookings";
+import { router as auth } from "./auth";
export const router = {
demo,
treatments,
bookings,
+ auth,
};