Files
rxminder/contexts/UserContext.tsx
2025-09-23 10:15:57 -07:00

238 lines
6.1 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from 'react';
import { User } from '../types';
import { databaseService } from '../services/database';
import { authService } from '../services/auth/auth.service';
import { tokenStorage } from '../utils/token';
import { logger } from '../services/logging';
import { normalizeError } from '../utils/error';
const SESSION_KEY = 'medication_app_session';
const AUTH_CONTEXT = 'USER_CONTEXT';
interface UserContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<boolean>;
register: (
email: string,
password: string,
username?: string
) => Promise<boolean>;
loginWithOAuth: (
provider: 'google' | 'github',
userData: { email: string; username: string; avatar?: string }
) => Promise<boolean>;
changePassword: (
currentPassword: string,
newPassword: string
) => Promise<boolean>;
logout: () => void;
updateUser: (
updatedUser: Omit<User, '_rev'> & { _rev: string }
) => Promise<void>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
try {
const sessionUser = localStorage.getItem(SESSION_KEY);
if (sessionUser) {
setUser(JSON.parse(sessionUser));
}
} catch {
// silent fail
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (user) {
localStorage.setItem(SESSION_KEY, JSON.stringify(user));
} else {
localStorage.removeItem(SESSION_KEY);
}
}, [user]);
const login = async (email: string, password: string): Promise<boolean> => {
try {
// Use auth service for password-based login
const result = await authService.login({ email, password });
// Update last login time
const updatedUser = { ...result.user, lastLoginAt: new Date() };
await databaseService.updateUser(updatedUser);
tokenStorage.save({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
});
// Set the user from the login result
setUser(updatedUser);
logger.auth.login('User authenticated with email/password', {
userId: updatedUser._id,
email: updatedUser.email,
});
return true;
} catch (error) {
logger.auth.error('Login error', normalizeError(error), { email });
return false;
}
};
const register = async (
email: string,
password: string,
username?: string
): Promise<boolean> => {
try {
await authService.register(email, password, username);
// Don't auto-login after registration, require email verification
return true;
} catch (error) {
logger.auth.error('Registration error', normalizeError(error), {
email,
username,
});
return false;
}
};
const loginWithOAuth = async (
provider: 'google' | 'github',
userData: { email: string; username: string; avatar?: string }
): Promise<boolean> => {
try {
const result = await authService.loginWithOAuth(provider, userData);
// Update last login time
const updatedUser = { ...result.user, lastLoginAt: new Date() };
await databaseService.updateUser(updatedUser);
tokenStorage.save({
accessToken: result.accessToken,
refreshToken: result.refreshToken,
});
setUser(updatedUser);
logger.auth.login('User authenticated via OAuth', {
userId: updatedUser._id,
provider,
email: updatedUser.email,
});
return true;
} catch (error) {
logger.auth.error('OAuth login error', normalizeError(error), {
provider,
email: userData.email,
});
return false;
}
};
const changePassword = async (
currentPassword: string,
newPassword: string
): Promise<boolean> => {
try {
if (!user) {
throw new Error('No user logged in');
}
await authService.changePassword(user._id, currentPassword, newPassword);
logger.auth.login('User changed password', { userId: user._id });
return true;
} catch (error) {
logger.auth.error('Password change error', normalizeError(error), {
userId: user?._id,
});
return false;
}
};
const logout = () => {
const currentUserId = user?._id;
tokenStorage.clear();
setUser(null);
logger.auth.logout('User logged out', { userId: currentUserId });
};
const updateUser = async (updatedUser: User) => {
try {
const savedUser = await databaseService.updateUser(updatedUser);
setUser(savedUser);
} catch (error) {
logger.error(
'Failed to update user profile',
AUTH_CONTEXT,
{ userId: updatedUser._id },
normalizeError(error)
);
}
};
if (isLoading) {
return (
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900'>
<PillIcon className='w-12 h-12 text-indigo-500 animate-spin' />
</div>
);
}
return (
<UserContext.Provider
value={{
user,
isLoading: false,
login,
register,
loginWithOAuth,
changePassword,
logout,
updateUser,
}}
>
{children}
</UserContext.Provider>
);
};
export const useUser = (): UserContextType => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
};
// Dummy icon for loading screen
const PillIcon: React.FC<React.SVGProps<SVGSVGElement>> = props => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='m10.5 20.5 10-10a4.95 4.95 0 1 0-7-7l-10 10a4.95 4.95 0 1 0 7 7Z' />
<path d='m8.5 8.5 7 7' />
</svg>
);