feat: implement user authentication with login and register

- Add login screen with email/username support
- Add register screen with email validation
- Implement AuthStore with expo-secure-store (native) / localStorage (web)
- Add X-User-Id header authentication (simple auth without JWT)
- Rename displayName to userName across codebase
- Add findByUserName() to UserRepository
- Check for existing email AND username on registration
- Add AuthButton component with shadow effect
- Add logout button to Header
- Add hash-password.js utility script for manual password resets
- Update CORS to allow X-User-Id header
This commit is contained in:
2026-01-10 20:07:35 +01:00
parent 71f84d1cc7
commit 8efe6c304e
20 changed files with 468 additions and 108 deletions

View File

@@ -1,26 +1,69 @@
import { create } from "zustand";
import { Platform } from "react-native";
import { User } from "@caldav/shared";
import * as SecureStore from "expo-secure-store";
const USER_STORAGE_KEY = "auth_user";
// SecureStore doesn't work on web, use localStorage as fallback
const storage = {
async setItem(key: string, value: string): Promise<void> {
if (Platform.OS === "web") {
localStorage.setItem(key, value);
} else {
await SecureStore.setItemAsync(key, value);
}
},
async getItem(key: string): Promise<string | null> {
if (Platform.OS === "web") {
return localStorage.getItem(key);
}
return SecureStore.getItemAsync(key);
},
async deleteItem(key: string): Promise<void> {
if (Platform.OS === "web") {
localStorage.removeItem(key);
} else {
await SecureStore.deleteItemAsync(key);
}
},
};
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (user: User, token: string) => void;
logout: () => void;
setToken: (token: string) => void;
isLoading: boolean;
login: (user: User) => Promise<void>;
logout: () => Promise<void>;
loadStoredUser: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
token: null,
isAuthenticated: false,
login: (_user: User, _token: string) => {
throw new Error("Not implemented");
isLoading: true,
login: async (user: User) => {
await storage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
set({ user, isAuthenticated: true });
},
logout: () => {
throw new Error("Not implemented");
logout: async () => {
await storage.deleteItem(USER_STORAGE_KEY);
set({ user: null, isAuthenticated: false });
},
setToken: (_token: string) => {
throw new Error("Not implemented");
loadStoredUser: async () => {
try {
const stored = await storage.getItem(USER_STORAGE_KEY);
if (stored) {
const user = JSON.parse(stored) as User;
set({ user, isAuthenticated: true, isLoading: false });
} else {
set({ isLoading: false });
}
} catch {
set({ isLoading: false });
}
},
}));