Refactoring, regression testing until Phase 1 end.
This commit is contained in:
@@ -246,6 +246,7 @@ interface LoginResponse {
|
||||
refresh_token: string;
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: 'superuser' | 'manager' | 'developer' | 'top_brass';
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import LoginForm from '$lib/components/Auth/LoginForm.svelte';
|
||||
import { login, auth } from '$lib/stores/auth';
|
||||
import { login, isLoading, authError, clearAuthError } from '$lib/stores/auth';
|
||||
import { LayoutDashboard } from 'lucide-svelte';
|
||||
|
||||
async function handleLogin(event: CustomEvent<{ email: string; password: string }>) {
|
||||
const { email, password } = event.detail;
|
||||
clearAuthError();
|
||||
|
||||
const result = await login({ email, password });
|
||||
|
||||
if (result.success) {
|
||||
@@ -40,8 +42,8 @@
|
||||
|
||||
<LoginForm
|
||||
on:login={handleLogin}
|
||||
isLoading={$auth.isLoading}
|
||||
errorMessage={$auth.error}
|
||||
isLoading={$isLoading}
|
||||
errorMessage={$authError}
|
||||
/>
|
||||
|
||||
<div class="divider text-base-content/40 text-sm">Demo Access</div>
|
||||
|
||||
Reference in New Issue
Block a user