Files
rxminder/contexts/UserContext.tsx
William Valentin ac3643f76d Refactor database services and add component tests
- Remove deprecated CouchDB service files
- Update database test configurations
- Add test files for components and auth modules
- Update user context and admin interface
- Remove migration script for unified config
- Fix User interface properties in tests (use status instead of isActive)
2025-09-08 18:30:43 -07:00

220 lines
5.6 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';
const SESSION_KEY = 'medication_app_session';
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 });
console.warn('Login result received:', result);
console.warn('User from login:', result.user);
console.warn('User _id:', result.user._id);
// Update last login time
const updatedUser = { ...result.user, lastLoginAt: new Date() };
await databaseService.updateUser(updatedUser);
console.warn('Updated user with last login:', updatedUser);
// Store access token for subsequent API calls.
localStorage.setItem('access_token', result.accessToken);
// Set the user from the login result
setUser(updatedUser);
console.warn('User set in context');
return true;
} catch (error) {
console.error('Login error:', error);
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) {
console.error('Registration error:', error);
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);
console.warn('OAuth login result received:', result);
console.warn('OAuth user:', result.user);
console.warn('OAuth user _id:', result.user._id);
// Update last login time
const updatedUser = { ...result.user, lastLoginAt: new Date() };
await databaseService.updateUser(updatedUser);
console.warn('Updated OAuth user with last login:', updatedUser);
localStorage.setItem('access_token', result.accessToken);
setUser(updatedUser);
console.warn('OAuth user set in context');
return true;
} catch (error) {
console.error('OAuth login error:', error);
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);
return true;
} catch (error) {
console.error('Password change error:', error);
return false;
}
};
const logout = () => {
setUser(null);
};
const updateUser = async (updatedUser: User) => {
try {
const savedUser = await databaseService.updateUser(updatedUser);
setUser(savedUser);
} catch (error) {
console.error('Failed to update user', error);
// Optionally revert state or show 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>
);