|
|
|
|
@@ -3,27 +3,84 @@
|
|
|
|
|
*
|
|
|
|
|
* Svelte store for user authentication state, token management,
|
|
|
|
|
* and user profile information.
|
|
|
|
|
*
|
|
|
|
|
* Features:
|
|
|
|
|
* - Centralized authentication state machine
|
|
|
|
|
* - Consistent error message handling
|
|
|
|
|
* - Persistent storage integration
|
|
|
|
|
* - Type-safe state transitions
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { writable, derived } from 'svelte/store';
|
|
|
|
|
import { writable, derived, get } from 'svelte/store';
|
|
|
|
|
import { browser } from '$app/environment';
|
|
|
|
|
import { authApi, setTokens, clearTokens, getAccessToken } from '$lib/services/api';
|
|
|
|
|
|
|
|
|
|
// User type
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Types
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
export type UserRole = 'superuser' | 'manager' | 'developer' | 'top_brass';
|
|
|
|
|
|
|
|
|
|
export interface User {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
email: string;
|
|
|
|
|
role: 'superuser' | 'manager' | 'developer' | 'top_brass';
|
|
|
|
|
role: UserRole;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auth state type
|
|
|
|
|
export type AuthStatus = 'idle' | 'loading' | 'authenticated' | 'unauthenticated' | 'error';
|
|
|
|
|
|
|
|
|
|
export interface AuthState {
|
|
|
|
|
isAuthenticated: boolean;
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
status: AuthStatus;
|
|
|
|
|
error: string | null;
|
|
|
|
|
lastErrorAt: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User store
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Error Messages
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
const ERROR_MESSAGES = {
|
|
|
|
|
INVALID_CREDENTIALS: 'Invalid email or password. Please try again.',
|
|
|
|
|
NETWORK_ERROR: 'Unable to connect to the server. Please check your connection.',
|
|
|
|
|
SERVER_ERROR: 'An error occurred on the server. Please try again later.',
|
|
|
|
|
SESSION_EXPIRED: 'Your session has expired. Please log in again.',
|
|
|
|
|
UNAUTHORIZED: 'You are not authorized to access this resource.',
|
|
|
|
|
UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.',
|
|
|
|
|
VALIDATION_ERROR: 'Please check your input and try again.',
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
function getErrorMessage(error: unknown): string {
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
// Map known error patterns to consistent messages
|
|
|
|
|
const message = error.message.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (message.includes('invalid credentials') || message.includes('unauthorized')) {
|
|
|
|
|
return ERROR_MESSAGES.INVALID_CREDENTIALS;
|
|
|
|
|
}
|
|
|
|
|
if (message.includes('network') || message.includes('fetch')) {
|
|
|
|
|
return ERROR_MESSAGES.NETWORK_ERROR;
|
|
|
|
|
}
|
|
|
|
|
if (message.includes('timeout') || message.includes('504') || message.includes('503')) {
|
|
|
|
|
return ERROR_MESSAGES.SERVER_ERROR;
|
|
|
|
|
}
|
|
|
|
|
if (message.includes('session') || message.includes('expired')) {
|
|
|
|
|
return ERROR_MESSAGES.SESSION_EXPIRED;
|
|
|
|
|
}
|
|
|
|
|
if (message.includes('validation') || message.includes('invalid')) {
|
|
|
|
|
return ERROR_MESSAGES.VALIDATION_ERROR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return error.message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ERROR_MESSAGES.UNKNOWN_ERROR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Stores
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function createUserStore() {
|
|
|
|
|
const { subscribe, set, update } = writable<User | null>(null);
|
|
|
|
|
|
|
|
|
|
@@ -37,64 +94,79 @@ function createUserStore() {
|
|
|
|
|
|
|
|
|
|
export const user = createUserStore();
|
|
|
|
|
|
|
|
|
|
// Authentication state store
|
|
|
|
|
function createAuthStore() {
|
|
|
|
|
const { subscribe, set, update } = writable<AuthState>({
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
status: 'idle',
|
|
|
|
|
error: null,
|
|
|
|
|
lastErrorAt: null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
subscribe,
|
|
|
|
|
set,
|
|
|
|
|
update,
|
|
|
|
|
setLoading: (loading: boolean) => update((state) => ({ ...state, isLoading: loading })),
|
|
|
|
|
setError: (error: string | null) => update((state) => ({ ...state, error })),
|
|
|
|
|
|
|
|
|
|
// State transitions
|
|
|
|
|
setIdle: () => set({ status: 'idle', error: null, lastErrorAt: null }),
|
|
|
|
|
setLoading: () => update((state) => ({ ...state, status: 'loading', error: null })),
|
|
|
|
|
setAuthenticated: () => set({ status: 'authenticated', error: null, lastErrorAt: null }),
|
|
|
|
|
setUnauthenticated: () => set({ status: 'unauthenticated', error: null, lastErrorAt: null }),
|
|
|
|
|
setError: (error: unknown) => {
|
|
|
|
|
const errorMessage = getErrorMessage(error);
|
|
|
|
|
set({
|
|
|
|
|
status: 'error',
|
|
|
|
|
error: errorMessage,
|
|
|
|
|
lastErrorAt: Date.now()
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Utility
|
|
|
|
|
clearError: () => update((state) => ({ ...state, error: null })),
|
|
|
|
|
setAuthenticated: (authenticated: boolean) => update((state) => ({ ...state, isAuthenticated: authenticated })),
|
|
|
|
|
|
|
|
|
|
// Getters
|
|
|
|
|
getState: () => get({ subscribe }),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const auth = createAuthStore();
|
|
|
|
|
|
|
|
|
|
// Derived store to check if user is authenticated
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Derived Stores
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/** Check if user is authenticated */
|
|
|
|
|
export const isAuthenticated = derived(
|
|
|
|
|
[user, auth],
|
|
|
|
|
([$user, $auth]) => $user !== null && $auth.isAuthenticated
|
|
|
|
|
([$user, $auth]) => $user !== null && $auth.status === 'authenticated'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Derived store to get user role
|
|
|
|
|
/** Get current user role */
|
|
|
|
|
export const userRole = derived(user, ($user) => $user?.role || null);
|
|
|
|
|
|
|
|
|
|
// Initialize auth state from localStorage (client-side only)
|
|
|
|
|
export function initAuth(): void {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
/** Check if auth is in loading state */
|
|
|
|
|
export const isLoading = derived(auth, ($auth) => $auth.status === 'loading');
|
|
|
|
|
|
|
|
|
|
const token = getAccessToken();
|
|
|
|
|
if (token) {
|
|
|
|
|
auth.setAuthenticated(true);
|
|
|
|
|
// Optionally fetch user profile here
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/** Get current error message */
|
|
|
|
|
export const authError = derived(auth, ($auth) => $auth.error);
|
|
|
|
|
|
|
|
|
|
// Login credentials type
|
|
|
|
|
interface LoginCredentials {
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Actions
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
export interface LoginCredentials {
|
|
|
|
|
email: string;
|
|
|
|
|
password: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Login result type
|
|
|
|
|
interface LoginResult {
|
|
|
|
|
export interface LoginResult {
|
|
|
|
|
success: boolean;
|
|
|
|
|
user?: User;
|
|
|
|
|
error?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Login function
|
|
|
|
|
/**
|
|
|
|
|
* Login action
|
|
|
|
|
*/
|
|
|
|
|
export async function login(credentials: LoginCredentials): Promise<LoginResult> {
|
|
|
|
|
auth.setLoading(true);
|
|
|
|
|
auth.clearError();
|
|
|
|
|
auth.setLoading();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await authApi.login(credentials);
|
|
|
|
|
@@ -102,23 +174,23 @@ export async function login(credentials: LoginCredentials): Promise<LoginResult>
|
|
|
|
|
if (response.access_token && response.refresh_token) {
|
|
|
|
|
setTokens(response.access_token, response.refresh_token);
|
|
|
|
|
user.set(response.user || null);
|
|
|
|
|
auth.setAuthenticated(true);
|
|
|
|
|
auth.setAuthenticated();
|
|
|
|
|
return { success: true, user: response.user };
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Invalid response from server');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : 'Login failed';
|
|
|
|
|
auth.setError(errorMessage);
|
|
|
|
|
const errorMessage = getErrorMessage(error);
|
|
|
|
|
auth.setError(error);
|
|
|
|
|
return { success: false, error: errorMessage };
|
|
|
|
|
} finally {
|
|
|
|
|
auth.setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Logout function
|
|
|
|
|
/**
|
|
|
|
|
* Logout action
|
|
|
|
|
*/
|
|
|
|
|
export async function logout(): Promise<void> {
|
|
|
|
|
auth.setLoading(true);
|
|
|
|
|
auth.setLoading();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await authApi.logout();
|
|
|
|
|
@@ -127,53 +199,73 @@ export async function logout(): Promise<void> {
|
|
|
|
|
} finally {
|
|
|
|
|
clearTokens();
|
|
|
|
|
user.clear();
|
|
|
|
|
auth.setAuthenticated(false);
|
|
|
|
|
auth.setLoading(false);
|
|
|
|
|
auth.setUnauthenticated();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check authentication status
|
|
|
|
|
/**
|
|
|
|
|
* Check authentication status
|
|
|
|
|
*/
|
|
|
|
|
export async function checkAuth(): Promise<boolean> {
|
|
|
|
|
if (!browser) return false;
|
|
|
|
|
|
|
|
|
|
const token = getAccessToken();
|
|
|
|
|
if (!token) {
|
|
|
|
|
auth.setAuthenticated(false);
|
|
|
|
|
auth.setUnauthenticated();
|
|
|
|
|
user.clear();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auth.setAuthenticated(true);
|
|
|
|
|
// If we have a token but no user, we're in a "restoring" state
|
|
|
|
|
const currentUser = get(user);
|
|
|
|
|
if (!currentUser) {
|
|
|
|
|
// Token exists but user data is missing - try to restore from token
|
|
|
|
|
// For now, we mark as authenticated and let the app fetch user data
|
|
|
|
|
auth.setAuthenticated();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Role check helpers
|
|
|
|
|
export function hasRole(role: string): boolean {
|
|
|
|
|
let currentRole: string | null = null;
|
|
|
|
|
const unsubscribe = userRole.subscribe((r) => {
|
|
|
|
|
currentRole = r;
|
|
|
|
|
});
|
|
|
|
|
unsubscribe();
|
|
|
|
|
return currentRole === role;
|
|
|
|
|
/**
|
|
|
|
|
* Initialize auth state from storage
|
|
|
|
|
*/
|
|
|
|
|
export function initAuth(): void {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
|
|
|
|
|
const token = getAccessToken();
|
|
|
|
|
if (token) {
|
|
|
|
|
auth.setAuthenticated();
|
|
|
|
|
} else {
|
|
|
|
|
auth.setUnauthenticated();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isSuperuser(): boolean {
|
|
|
|
|
return hasRole('superuser');
|
|
|
|
|
/**
|
|
|
|
|
* Clear any authentication error
|
|
|
|
|
*/
|
|
|
|
|
export function clearAuthError(): void {
|
|
|
|
|
auth.clearError();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isManager(): boolean {
|
|
|
|
|
return hasRole('manager');
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Role Helpers
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
export function hasRole(role: UserRole): boolean {
|
|
|
|
|
const currentUser = get(user);
|
|
|
|
|
return currentUser?.role === role;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isDeveloper(): boolean {
|
|
|
|
|
return hasRole('developer');
|
|
|
|
|
}
|
|
|
|
|
export const isSuperuser = () => hasRole('superuser');
|
|
|
|
|
export const isManager = () => hasRole('manager');
|
|
|
|
|
export const isDeveloper = () => hasRole('developer');
|
|
|
|
|
export const isTopBrass = () => hasRole('top_brass');
|
|
|
|
|
|
|
|
|
|
export function isTopBrass(): boolean {
|
|
|
|
|
return hasRole('top_brass');
|
|
|
|
|
}
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Permission Helpers
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
// Permission check (can be expanded based on requirements)
|
|
|
|
|
export function canManageTeamMembers(): boolean {
|
|
|
|
|
return isSuperuser() || isManager();
|
|
|
|
|
}
|
|
|
|
|
|