backstory/frontend/src/hooks/useUser.tsx

164 lines
4.9 KiB
TypeScript

import React, { createContext, useContext, useEffect, useState } from "react";
import { SetSnackType } from '../components/Snack';
import { User, Guest, Candidate } from 'types/types';
import { ApiClient } from "services/api-client";
import { debugConversion } from "types/conversion";
type UserContextType = {
apiClient: ApiClient;
user: User | null;
guest: Guest;
candidate: Candidate | null;
setUser: (user: User | null) => void;
setCandidate: (candidate: Candidate | null) => void;
};
const UserContext = createContext<UserContextType | undefined>(undefined);
const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) throw new Error("useUser must be used within a UserProvider");
return ctx;
};
interface UserProviderProps {
children: React.ReactNode;
setSnack: SetSnackType;
};
const UserProvider: React.FC<UserProviderProps> = (props: UserProviderProps) => {
const { children, setSnack } = props;
const [apiClient, setApiClient] = useState<ApiClient>(new ApiClient());
const [candidate, setCandidate] = useState<Candidate | null>(null);
const [guest, setGuest] = useState<Guest | null>(null);
const [user, setUser] = useState<User | null>(null);
const [activeUser, setActiveUser] = useState<User | null>(null);
useEffect(() => {
console.log("Candidate =>", candidate);
}, [candidate]);
useEffect(() => {
console.log("Guest =>", guest);
}, [guest]);
/* If the user changes to a non-null value, create a new
* apiClient with the access token */
useEffect(() => {
console.log("User => ", user);
if (user === null) {
return;
}
/* This apiClient will persist until the user is changed
* or logged out */
const accessToken = localStorage.getItem('accessToken');
if (!accessToken) {
throw Error("accessToken is not set for user!");
}
setApiClient(new ApiClient(accessToken));
}, [user]);
/* Handle logout if any consumers of UserProvider setUser to NULL */
useEffect(() => {
/* If there is an active user and it is the same as the
* new user, do nothing */
if (activeUser && activeUser.email === user?.email) {
return;
}
const logout = async () => {
if (!activeUser) {
return;
}
console.log(`Logging out ${activeUser.email}`);
try {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (!accessToken || !refreshToken) {
setSnack("Authentication tokens are invalid.", "error");
return;
}
const results = await apiClient.logout(accessToken, refreshToken);
if (results.error) {
console.error(results.error);
setSnack(results.error.message, "error")
}
} catch (e) {
console.error(e);
setSnack(`Unable to logout: ${e}`, "error")
}
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('userData');
createGuestSession();
setUser(null);
};
setActiveUser(user);
if (!user) {
logout();
}
}, [user, apiClient, activeUser]);
const createGuestSession = () => {
console.log("TODO: Convert this to query the server for the session instead of generating it.");
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const guest: Guest = {
sessionId,
createdAt: new Date(),
lastActivity: new Date(),
ipAddress: 'unknown',
userAgent: navigator.userAgent
};
setGuest(guest);
debugConversion(guest, 'Guest Session');
};
const checkExistingAuth = () => {
const accessToken = localStorage.getItem('accessToken');
const userData = localStorage.getItem('userData');
if (accessToken && userData) {
try {
const user = JSON.parse(userData);
// Convert dates back to Date objects if they're stored as strings
if (user.createdAt && typeof user.createdAt === 'string') {
user.createdAt = new Date(user.createdAt);
}
if (user.updatedAt && typeof user.updatedAt === 'string') {
user.updatedAt = new Date(user.updatedAt);
}
if (user.lastLogin && typeof user.lastLogin === 'string') {
user.lastLogin = new Date(user.lastLogin);
}
setApiClient(new ApiClient(accessToken));
setUser(user);
} catch (e) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('userData');
}
}
};
// Create guest session on component mount
useEffect(() => {
createGuestSession();
checkExistingAuth();
}, []);
if (guest === null) {
return <></>;
}
return (
<UserContext.Provider value={{ apiClient, candidate, setCandidate, user, setUser, guest }}>
{children}
</UserContext.Provider>
);
};
export {
UserProvider,
useUser
};