diff --git a/src/App.tsx b/src/App.tsx
index 8593236f..564ee1a3 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,11 +1,10 @@
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import { ThemeProvider } from './contexts/ThemeContext';
-import { AuthProvider } from './contexts/AuthContext';
+import { AuthProvider, ProtectedRoute } from './components/auth';
import { TaskMasterProvider } from './contexts/TaskMasterContext';
import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
import { WebSocketProvider } from './contexts/WebSocketContext';
-import ProtectedRoute from './components/ProtectedRoute';
import AppContent from './components/app/AppContent';
import i18n from './i18n/config.js';
diff --git a/src/components/LoginForm.jsx b/src/components/LoginForm.jsx
deleted file mode 100644
index 1482ad7d..00000000
--- a/src/components/LoginForm.jsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import React, { useState } from 'react';
-import { useAuth } from '../contexts/AuthContext';
-import { MessageSquare } from 'lucide-react';
-import { useTranslation } from 'react-i18next';
-
-const LoginForm = () => {
- const { t } = useTranslation('auth');
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState('');
-
- const { login } = useAuth();
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setError('');
-
- if (!username || !password) {
- setError(t('errors.requiredFields'));
- return;
- }
-
- setIsLoading(true);
-
- const result = await login(username, password);
-
- if (!result.success) {
- setError(result.error);
- }
-
- setIsLoading(false);
- };
-
- return (
-
-
-
- {/* Logo and Title */}
-
-
-
{t('login.title')}
-
- {t('login.description')}
-
-
-
- {/* Login Form */}
-
-
-
-
- Enter your credentials to access Claude Code UI
-
-
-
-
-
- );
-};
-
-export default LoginForm;
\ No newline at end of file
diff --git a/src/components/Onboarding.jsx b/src/components/Onboarding.jsx
index 15fed04c..556e3c77 100644
--- a/src/components/Onboarding.jsx
+++ b/src/components/Onboarding.jsx
@@ -3,7 +3,7 @@ import { ChevronRight, ChevronLeft, Check, GitBranch, User, Mail, LogIn, Externa
import SessionProviderLogo from './llm-logo-provider/SessionProviderLogo';
import LoginModal from './LoginModal';
import { authenticatedFetch } from '../utils/api';
-import { useAuth } from '../contexts/AuthContext';
+import { useAuth } from './auth/context/AuthContext';
import { IS_PLATFORM } from '../constants/config';
const Onboarding = ({ onComplete }) => {
diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx
deleted file mode 100644
index 507efa87..00000000
--- a/src/components/ProtectedRoute.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react';
-import { useAuth } from '../contexts/AuthContext';
-import SetupForm from './SetupForm';
-import LoginForm from './LoginForm';
-import Onboarding from './Onboarding';
-import { MessageSquare } from 'lucide-react';
-import { IS_PLATFORM } from '../constants/config';
-
-const LoadingScreen = () => (
-
-
-
-
Claude Code UI
-
-
Loading...
-
-
-);
-
-const ProtectedRoute = ({ children }) => {
- const { user, isLoading, needsSetup, hasCompletedOnboarding, refreshOnboardingStatus } = useAuth();
-
- if (IS_PLATFORM) {
- if (isLoading) {
- return ;
- }
-
- if (!hasCompletedOnboarding) {
- return ;
- }
-
- return children;
- }
-
- if (isLoading) {
- return ;
- }
-
- if (needsSetup) {
- return ;
- }
-
- if (!user) {
- return ;
- }
-
- if (!hasCompletedOnboarding) {
- return ;
- }
-
- return children;
-};
-
-export default ProtectedRoute;
\ No newline at end of file
diff --git a/src/components/SetupForm.jsx b/src/components/SetupForm.jsx
deleted file mode 100644
index 4f41d0b7..00000000
--- a/src/components/SetupForm.jsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import React, { useState } from 'react';
-import { useAuth } from '../contexts/AuthContext';
-const SetupForm = () => {
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState('');
-
- const { register } = useAuth();
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setError('');
-
- if (password !== confirmPassword) {
- setError('Passwords do not match');
- return;
- }
-
- if (username.length < 3) {
- setError('Username must be at least 3 characters long');
- return;
- }
-
- if (password.length < 6) {
- setError('Password must be at least 6 characters long');
- return;
- }
-
- setIsLoading(true);
-
- const result = await register(username, password);
-
- if (!result.success) {
- setError(result.error);
- }
-
- setIsLoading(false);
- };
-
- return (
-
-
-
- {/* Logo and Title */}
-
-
-

-
-
Welcome to Claude Code UI
-
- Set up your account to get started
-
-
-
- {/* Setup Form */}
-
-
-
-
- This is a single-user system. Only one account can be created.
-
-
-
-
-
- );
-};
-
-export default SetupForm;
\ No newline at end of file
diff --git a/src/components/auth/constants.ts b/src/components/auth/constants.ts
new file mode 100644
index 00000000..37226b02
--- /dev/null
+++ b/src/components/auth/constants.ts
@@ -0,0 +1,8 @@
+export const AUTH_TOKEN_STORAGE_KEY = 'auth-token';
+
+export const AUTH_ERROR_MESSAGES = {
+ authStatusCheckFailed: 'Failed to check authentication status',
+ loginFailed: 'Login failed',
+ registrationFailed: 'Registration failed',
+ networkError: 'Network error. Please try again.',
+} as const;
diff --git a/src/components/auth/context/AuthContext.tsx b/src/components/auth/context/AuthContext.tsx
new file mode 100644
index 00000000..69bdd8a5
--- /dev/null
+++ b/src/components/auth/context/AuthContext.tsx
@@ -0,0 +1,222 @@
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { IS_PLATFORM } from '../../../constants/config';
+import { api } from '../../../utils/api';
+import { AUTH_ERROR_MESSAGES, AUTH_TOKEN_STORAGE_KEY } from '../constants';
+import type {
+ AuthContextValue,
+ AuthProviderProps,
+ AuthSessionPayload,
+ AuthStatusPayload,
+ AuthUser,
+ AuthUserPayload,
+ OnboardingStatusPayload,
+} from '../types';
+import { parseJsonSafely, resolveApiErrorMessage } from '../utils';
+
+const AuthContext = createContext(null);
+
+const readStoredToken = (): string | null => localStorage.getItem(AUTH_TOKEN_STORAGE_KEY);
+
+const persistToken = (token: string) => {
+ localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, token);
+};
+
+const clearStoredToken = () => {
+ localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY);
+};
+
+export function useAuth(): AuthContextValue {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+
+ return context;
+}
+
+export function AuthProvider({ children }: AuthProviderProps) {
+ const [user, setUser] = useState(null);
+ const [token, setToken] = useState(() => readStoredToken());
+ const [isLoading, setIsLoading] = useState(true);
+ const [needsSetup, setNeedsSetup] = useState(false);
+ const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState(true);
+ const [error, setError] = useState(null);
+
+ const setSession = useCallback((nextUser: AuthUser, nextToken: string) => {
+ setUser(nextUser);
+ setToken(nextToken);
+ persistToken(nextToken);
+ }, []);
+
+ const clearSession = useCallback(() => {
+ setUser(null);
+ setToken(null);
+ clearStoredToken();
+ }, []);
+
+ const checkOnboardingStatus = useCallback(async () => {
+ try {
+ const response = await api.user.onboardingStatus();
+ if (!response.ok) {
+ return;
+ }
+
+ const payload = await parseJsonSafely(response);
+ setHasCompletedOnboarding(Boolean(payload?.hasCompletedOnboarding));
+ } catch (caughtError) {
+ console.error('Error checking onboarding status:', caughtError);
+ // Fail open to avoid blocking access on transient onboarding status errors.
+ setHasCompletedOnboarding(true);
+ }
+ }, []);
+
+ const refreshOnboardingStatus = useCallback(async () => {
+ await checkOnboardingStatus();
+ }, [checkOnboardingStatus]);
+
+ const checkAuthStatus = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const statusResponse = await api.auth.status();
+ const statusPayload = await parseJsonSafely(statusResponse);
+
+ if (statusPayload?.needsSetup) {
+ setNeedsSetup(true);
+ return;
+ }
+
+ setNeedsSetup(false);
+
+ if (!token) {
+ return;
+ }
+
+ const userResponse = await api.auth.user();
+ if (!userResponse.ok) {
+ clearSession();
+ return;
+ }
+
+ const userPayload = await parseJsonSafely(userResponse);
+ if (!userPayload?.user) {
+ clearSession();
+ return;
+ }
+
+ setUser(userPayload.user);
+ await checkOnboardingStatus();
+ } catch (caughtError) {
+ console.error('[Auth] Auth status check failed:', caughtError);
+ setError(AUTH_ERROR_MESSAGES.authStatusCheckFailed);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [checkOnboardingStatus, clearSession, token]);
+
+ useEffect(() => {
+ if (IS_PLATFORM) {
+ setUser({ username: 'platform-user' });
+ setNeedsSetup(false);
+ void checkOnboardingStatus().finally(() => {
+ setIsLoading(false);
+ });
+ return;
+ }
+
+ void checkAuthStatus();
+ }, [checkAuthStatus, checkOnboardingStatus]);
+
+ const login = useCallback(
+ async (username, password) => {
+ try {
+ setError(null);
+ const response = await api.auth.login(username, password);
+ const payload = await parseJsonSafely(response);
+
+ if (!response.ok || !payload?.token || !payload.user) {
+ const message = resolveApiErrorMessage(payload, AUTH_ERROR_MESSAGES.loginFailed);
+ setError(message);
+ return { success: false, error: message };
+ }
+
+ setSession(payload.user, payload.token);
+ setNeedsSetup(false);
+ await checkOnboardingStatus();
+ return { success: true };
+ } catch (caughtError) {
+ console.error('Login error:', caughtError);
+ setError(AUTH_ERROR_MESSAGES.networkError);
+ return { success: false, error: AUTH_ERROR_MESSAGES.networkError };
+ }
+ },
+ [checkOnboardingStatus, setSession],
+ );
+
+ const register = useCallback(
+ async (username, password) => {
+ try {
+ setError(null);
+ const response = await api.auth.register(username, password);
+ const payload = await parseJsonSafely(response);
+
+ if (!response.ok || !payload?.token || !payload.user) {
+ const message = resolveApiErrorMessage(payload, AUTH_ERROR_MESSAGES.registrationFailed);
+ setError(message);
+ return { success: false, error: message };
+ }
+
+ setSession(payload.user, payload.token);
+ setNeedsSetup(false);
+ await checkOnboardingStatus();
+ return { success: true };
+ } catch (caughtError) {
+ console.error('Registration error:', caughtError);
+ setError(AUTH_ERROR_MESSAGES.networkError);
+ return { success: false, error: AUTH_ERROR_MESSAGES.networkError };
+ }
+ },
+ [checkOnboardingStatus, setSession],
+ );
+
+ const logout = useCallback(() => {
+ const tokenToInvalidate = token;
+ clearSession();
+
+ if (tokenToInvalidate) {
+ void api.auth.logout().catch((caughtError: unknown) => {
+ console.error('Logout endpoint error:', caughtError);
+ });
+ }
+ }, [clearSession, token]);
+
+ const contextValue = useMemo(
+ () => ({
+ user,
+ token,
+ isLoading,
+ needsSetup,
+ hasCompletedOnboarding,
+ error,
+ login,
+ register,
+ logout,
+ refreshOnboardingStatus,
+ }),
+ [
+ error,
+ hasCompletedOnboarding,
+ isLoading,
+ login,
+ logout,
+ needsSetup,
+ refreshOnboardingStatus,
+ register,
+ token,
+ user,
+ ],
+ );
+
+ return {children};
+}
diff --git a/src/components/auth/index.ts b/src/components/auth/index.ts
new file mode 100644
index 00000000..62659ecf
--- /dev/null
+++ b/src/components/auth/index.ts
@@ -0,0 +1,2 @@
+export { AuthProvider, useAuth } from './context/AuthContext';
+export { default as ProtectedRoute } from './view/ProtectedRoute';
diff --git a/src/components/auth/types.ts b/src/components/auth/types.ts
new file mode 100644
index 00000000..e745a372
--- /dev/null
+++ b/src/components/auth/types.ts
@@ -0,0 +1,50 @@
+import type { ReactNode } from 'react';
+
+export type AuthUser = {
+ id?: number | string;
+ username: string;
+ [key: string]: unknown;
+};
+
+export type AuthActionResult = { success: true } | { success: false; error: string };
+
+export type AuthSessionPayload = {
+ token?: string;
+ user?: AuthUser;
+ error?: string;
+ message?: string;
+};
+
+export type AuthStatusPayload = {
+ needsSetup?: boolean;
+};
+
+export type AuthUserPayload = {
+ user?: AuthUser;
+};
+
+export type OnboardingStatusPayload = {
+ hasCompletedOnboarding?: boolean;
+};
+
+export type ApiErrorPayload = {
+ error?: string;
+ message?: string;
+};
+
+export type AuthContextValue = {
+ user: AuthUser | null;
+ token: string | null;
+ isLoading: boolean;
+ needsSetup: boolean;
+ hasCompletedOnboarding: boolean;
+ error: string | null;
+ login: (username: string, password: string) => Promise;
+ register: (username: string, password: string) => Promise;
+ logout: () => void;
+ refreshOnboardingStatus: () => Promise;
+};
+
+export type AuthProviderProps = {
+ children: ReactNode;
+};
diff --git a/src/components/auth/utils.ts b/src/components/auth/utils.ts
new file mode 100644
index 00000000..152a986c
--- /dev/null
+++ b/src/components/auth/utils.ts
@@ -0,0 +1,17 @@
+import type { ApiErrorPayload } from './types';
+
+export async function parseJsonSafely(response: Response): Promise {
+ try {
+ return (await response.json()) as T;
+ } catch {
+ return null;
+ }
+}
+
+export function resolveApiErrorMessage(payload: ApiErrorPayload | null, fallback: string): string {
+ if (!payload) {
+ return fallback;
+ }
+
+ return payload.error ?? payload.message ?? fallback;
+}
diff --git a/src/components/auth/view/AuthErrorAlert.tsx b/src/components/auth/view/AuthErrorAlert.tsx
new file mode 100644
index 00000000..8bc90f27
--- /dev/null
+++ b/src/components/auth/view/AuthErrorAlert.tsx
@@ -0,0 +1,15 @@
+type AuthErrorAlertProps = {
+ errorMessage: string;
+};
+
+export default function AuthErrorAlert({ errorMessage }: AuthErrorAlertProps) {
+ if (!errorMessage) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/auth/view/AuthInputField.tsx b/src/components/auth/view/AuthInputField.tsx
new file mode 100644
index 00000000..3f1ff1a5
--- /dev/null
+++ b/src/components/auth/view/AuthInputField.tsx
@@ -0,0 +1,37 @@
+type AuthInputFieldProps = {
+ id: string;
+ label: string;
+ value: string;
+ onChange: (nextValue: string) => void;
+ placeholder: string;
+ isDisabled: boolean;
+ type?: 'text' | 'password' | 'email';
+};
+
+export default function AuthInputField({
+ id,
+ label,
+ value,
+ onChange,
+ placeholder,
+ isDisabled,
+ type = 'text',
+}: AuthInputFieldProps) {
+ return (
+
+
+ onChange(event.target.value)}
+ className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ placeholder={placeholder}
+ required
+ disabled={isDisabled}
+ />
+
+ );
+}
diff --git a/src/components/auth/view/AuthLoadingScreen.tsx b/src/components/auth/view/AuthLoadingScreen.tsx
new file mode 100644
index 00000000..28702385
--- /dev/null
+++ b/src/components/auth/view/AuthLoadingScreen.tsx
@@ -0,0 +1,31 @@
+import { MessageSquare } from 'lucide-react';
+
+const loadingDotAnimationDelays = ['0s', '0.1s', '0.2s'];
+
+export default function AuthLoadingScreen() {
+ return (
+
+
+
+
+
Claude Code UI
+
+
+ {loadingDotAnimationDelays.map((delay) => (
+
+ ))}
+
+
+
Loading...
+
+
+ );
+}
diff --git a/src/components/auth/view/AuthScreenLayout.tsx b/src/components/auth/view/AuthScreenLayout.tsx
new file mode 100644
index 00000000..871ce514
--- /dev/null
+++ b/src/components/auth/view/AuthScreenLayout.tsx
@@ -0,0 +1,44 @@
+import type { ReactNode } from 'react';
+import { MessageSquare } from 'lucide-react';
+
+type AuthScreenLayoutProps = {
+ title: string;
+ description: string;
+ children: ReactNode;
+ footerText: string;
+ logo?: ReactNode;
+};
+
+export default function AuthScreenLayout({
+ title,
+ description,
+ children,
+ footerText,
+ logo,
+}: AuthScreenLayoutProps) {
+ return (
+
+
+
+
+
+ {logo ?? (
+
+
+
+ )}
+
+
{title}
+
{description}
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/src/components/auth/view/LoginForm.tsx b/src/components/auth/view/LoginForm.tsx
new file mode 100644
index 00000000..c3529543
--- /dev/null
+++ b/src/components/auth/view/LoginForm.tsx
@@ -0,0 +1,90 @@
+import { useCallback, useState } from 'react';
+import type { FormEvent } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useAuth } from '../context/AuthContext';
+import AuthErrorAlert from './AuthErrorAlert';
+import AuthInputField from './AuthInputField';
+import AuthScreenLayout from './AuthScreenLayout';
+
+type LoginFormState = {
+ username: string;
+ password: string;
+};
+
+const initialState: LoginFormState = {
+ username: '',
+ password: '',
+};
+
+export default function LoginForm() {
+ const { t } = useTranslation('auth');
+ const { login } = useAuth();
+
+ const [formState, setFormState] = useState(initialState);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const updateField = useCallback((field: keyof LoginFormState, value: string) => {
+ setFormState((previous) => ({ ...previous, [field]: value }));
+ }, []);
+
+ const handleSubmit = useCallback(
+ async (event: FormEvent) => {
+ event.preventDefault();
+ setErrorMessage('');
+
+ // Keep form validation local so each auth screen owns its own UI feedback.
+ if (!formState.username.trim() || !formState.password) {
+ setErrorMessage(t('login.errors.requiredFields'));
+ return;
+ }
+
+ setIsSubmitting(true);
+ const result = await login(formState.username.trim(), formState.password);
+ if (!result.success) {
+ setErrorMessage(result.error);
+ }
+ setIsSubmitting(false);
+ },
+ [formState.password, formState.username, login, t],
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/auth/view/ProtectedRoute.tsx b/src/components/auth/view/ProtectedRoute.tsx
new file mode 100644
index 00000000..10431e5c
--- /dev/null
+++ b/src/components/auth/view/ProtectedRoute.tsx
@@ -0,0 +1,41 @@
+import type { ReactNode } from 'react';
+import Onboarding from '../../Onboarding';
+import { IS_PLATFORM } from '../../../constants/config';
+import { useAuth } from '../context/AuthContext';
+import AuthLoadingScreen from './AuthLoadingScreen';
+import LoginForm from './LoginForm';
+import SetupForm from './SetupForm';
+
+type ProtectedRouteProps = {
+ children: ReactNode;
+};
+
+export default function ProtectedRoute({ children }: ProtectedRouteProps) {
+ const { user, isLoading, needsSetup, hasCompletedOnboarding, refreshOnboardingStatus } = useAuth();
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (IS_PLATFORM) {
+ if (!hasCompletedOnboarding) {
+ return ;
+ }
+
+ return <>{children}>;
+ }
+
+ if (needsSetup) {
+ return ;
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ if (!hasCompletedOnboarding) {
+ return ;
+ }
+
+ return <>{children}>;
+}
diff --git a/src/components/auth/view/SetupForm.tsx b/src/components/auth/view/SetupForm.tsx
new file mode 100644
index 00000000..f4d892d1
--- /dev/null
+++ b/src/components/auth/view/SetupForm.tsx
@@ -0,0 +1,121 @@
+import { useCallback, useState } from 'react';
+import type { FormEvent } from 'react';
+import { useAuth } from '../context/AuthContext';
+import AuthErrorAlert from './AuthErrorAlert';
+import AuthInputField from './AuthInputField';
+import AuthScreenLayout from './AuthScreenLayout';
+
+type SetupFormState = {
+ username: string;
+ password: string;
+ confirmPassword: string;
+};
+
+const initialState: SetupFormState = {
+ username: '',
+ password: '',
+ confirmPassword: '',
+};
+
+function validateSetupForm(formState: SetupFormState): string | null {
+ if (!formState.username.trim() || !formState.password || !formState.confirmPassword) {
+ return 'Please fill in all fields.';
+ }
+
+ if (formState.username.trim().length < 3) {
+ return 'Username must be at least 3 characters long.';
+ }
+
+ if (formState.password.length < 6) {
+ return 'Password must be at least 6 characters long.';
+ }
+
+ if (formState.password !== formState.confirmPassword) {
+ return 'Passwords do not match.';
+ }
+
+ return null;
+}
+
+export default function SetupForm() {
+ const { register } = useAuth();
+
+ const [formState, setFormState] = useState(initialState);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const updateField = useCallback((field: keyof SetupFormState, value: string) => {
+ setFormState((previous) => ({ ...previous, [field]: value }));
+ }, []);
+
+ const handleSubmit = useCallback(
+ async (event: FormEvent) => {
+ event.preventDefault();
+ setErrorMessage('');
+
+ const validationError = validateSetupForm(formState);
+ if (validationError) {
+ setErrorMessage(validationError);
+ return;
+ }
+
+ setIsSubmitting(true);
+ const result = await register(formState.username.trim(), formState.password);
+ if (!result.success) {
+ setErrorMessage(result.error);
+ }
+ setIsSubmitting(false);
+ },
+ [formState, register],
+ );
+
+ return (
+ }
+ >
+
+
+ );
+}
diff --git a/src/components/task-master/context/TaskMasterContext.tsx b/src/components/task-master/context/TaskMasterContext.tsx
index 01f6e300..37953ad8 100644
--- a/src/components/task-master/context/TaskMasterContext.tsx
+++ b/src/components/task-master/context/TaskMasterContext.tsx
@@ -1,6 +1,6 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { api } from '../../../utils/api';
-import { useAuth } from '../../../contexts/AuthContext';
+import { useAuth } from '../../auth/context/AuthContext';
import { useWebSocket } from '../../../contexts/WebSocketContext';
import type {
TaskMasterContextError,
@@ -15,12 +15,6 @@ import type {
const TaskMasterContext = createContext(null);
-type AuthContextValue = {
- user: unknown;
- token: string | null;
- isLoading: boolean;
-};
-
function createTaskMasterError(context: string, error: unknown): TaskMasterContextError {
const message = error instanceof Error ? error.message : `Failed to ${context}`;
return {
@@ -64,7 +58,7 @@ export function useTaskMaster() {
export function TaskMasterProvider({ children }: { children: React.ReactNode }) {
const { latestMessage } = useWebSocket();
- const { user, token, isLoading: isAuthLoading } = useAuth() as AuthContextValue;
+ const { user, token, isLoading: isAuthLoading } = useAuth();
const [projects, setProjects] = useState([]);
const [currentProject, setCurrentProjectState] = useState(null);
diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx
index d974c7db..2673719d 100644
--- a/src/contexts/AuthContext.jsx
+++ b/src/contexts/AuthContext.jsx
@@ -1,189 +1 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
-import { api } from '../utils/api';
-import { IS_PLATFORM } from '../constants/config';
-
-const AuthContext = createContext({
- user: null,
- token: null,
- login: () => {},
- register: () => {},
- logout: () => {},
- isLoading: true,
- needsSetup: false,
- hasCompletedOnboarding: true,
- refreshOnboardingStatus: () => {},
- error: null
-});
-
-export const useAuth = () => {
- const context = useContext(AuthContext);
- if (!context) {
- throw new Error('useAuth must be used within an AuthProvider');
- }
- return context;
-};
-
-export const AuthProvider = ({ children }) => {
- const [user, setUser] = useState(null);
- const [token, setToken] = useState(localStorage.getItem('auth-token'));
- const [isLoading, setIsLoading] = useState(true);
- const [needsSetup, setNeedsSetup] = useState(false);
- const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (IS_PLATFORM) {
- setUser({ username: 'platform-user' });
- setNeedsSetup(false);
- checkOnboardingStatus();
- setIsLoading(false);
- return;
- }
-
- checkAuthStatus();
- }, []);
-
- const checkOnboardingStatus = async () => {
- try {
- const response = await api.user.onboardingStatus();
- if (response.ok) {
- const data = await response.json();
- setHasCompletedOnboarding(data.hasCompletedOnboarding);
- }
- } catch (error) {
- console.error('Error checking onboarding status:', error);
- setHasCompletedOnboarding(true);
- }
- };
-
- const refreshOnboardingStatus = async () => {
- await checkOnboardingStatus();
- };
-
- const checkAuthStatus = async () => {
- try {
- setIsLoading(true);
- setError(null);
-
- // Check if system needs setup
- const statusResponse = await api.auth.status();
- const statusData = await statusResponse.json();
-
- if (statusData.needsSetup) {
- setNeedsSetup(true);
- setIsLoading(false);
- return;
- }
-
- // If we have a token, verify it
- if (token) {
- try {
- const userResponse = await api.auth.user();
-
- if (userResponse.ok) {
- const userData = await userResponse.json();
- setUser(userData.user);
- setNeedsSetup(false);
- await checkOnboardingStatus();
- } else {
- // Token is invalid
- localStorage.removeItem('auth-token');
- setToken(null);
- setUser(null);
- }
- } catch (error) {
- console.error('Token verification failed:', error);
- localStorage.removeItem('auth-token');
- setToken(null);
- setUser(null);
- }
- }
- } catch (error) {
- console.error('[AuthContext] Auth status check failed:', error);
- setError('Failed to check authentication status');
- } finally {
- setIsLoading(false);
- }
- };
-
- const login = async (username, password) => {
- try {
- setError(null);
- const response = await api.auth.login(username, password);
-
- const data = await response.json();
-
- if (response.ok) {
- setToken(data.token);
- setUser(data.user);
- localStorage.setItem('auth-token', data.token);
- return { success: true };
- } else {
- setError(data.error || 'Login failed');
- return { success: false, error: data.error || 'Login failed' };
- }
- } catch (error) {
- console.error('Login error:', error);
- const errorMessage = 'Network error. Please try again.';
- setError(errorMessage);
- return { success: false, error: errorMessage };
- }
- };
-
- const register = async (username, password) => {
- try {
- setError(null);
- const response = await api.auth.register(username, password);
-
- const data = await response.json();
-
- if (response.ok) {
- setToken(data.token);
- setUser(data.user);
- setNeedsSetup(false);
- localStorage.setItem('auth-token', data.token);
- return { success: true };
- } else {
- setError(data.error || 'Registration failed');
- return { success: false, error: data.error || 'Registration failed' };
- }
- } catch (error) {
- console.error('Registration error:', error);
- const errorMessage = 'Network error. Please try again.';
- setError(errorMessage);
- return { success: false, error: errorMessage };
- }
- };
-
- const logout = () => {
- setToken(null);
- setUser(null);
- localStorage.removeItem('auth-token');
-
- // Optional: Call logout endpoint for logging
- if (token) {
- api.auth.logout().catch(error => {
- console.error('Logout endpoint error:', error);
- });
- }
- };
-
- const value = {
- user,
- token,
- login,
- register,
- logout,
- isLoading,
- needsSetup,
- hasCompletedOnboarding,
- refreshOnboardingStatus,
- error
- };
-
- return (
-
- {children}
-
- );
-};
\ No newline at end of file
+export { AuthProvider, useAuth } from '../components/auth/context/AuthContext';
diff --git a/src/contexts/WebSocketContext.tsx b/src/contexts/WebSocketContext.tsx
index ff680963..cd4e2a99 100644
--- a/src/contexts/WebSocketContext.tsx
+++ b/src/contexts/WebSocketContext.tsx
@@ -1,5 +1,5 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
-import { useAuth } from './AuthContext';
+import { useAuth } from '../components/auth/context/AuthContext';
import { IS_PLATFORM } from '../constants/config';
type WebSocketContextType = {