import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react'; import { ERRORS } from '../components/Error/Errors'; import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst'; import useGuestBookStore from '../store/store'; import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post'; import { User, UserCreate, UserDelete, UserImageUpdate, UserList, UserPermissionsUpdate, UserUpdate, } from '../types/User'; const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/'; interface ApiContext { hasAuth: boolean; authenticatedUser?: User; currentSession: [string?, string?]; logIn?: (email: string, password: string) => Promise; logOut?: () => Promise; posts?: (page?: number) => Promise; newPost?: (data: PostCreate) => Promise; updatePost?: (data: PostUpdate, id: number) => Promise; deletePost?: (id: number) => Promise; users?: (page?: number) => Promise; user?: (id?: number) => Promise; createUser?: (data: UserCreate) => Promise; confirmUser?: (code: string) => Promise; updateUser?: (data: UserUpdate, id?: number) => Promise; updateUserImage?: (data: UserImageUpdate, id?: number) => Promise; updateUserPermissions?: (data: UserPermissionsUpdate, id: number) => Promise; deleteUser?: (id: number) => Promise; userPosts?: (id?: number) => Promise; } const ApiContext = createContext({ hasAuth: false, currentSession: [undefined, undefined], }); //eslint-disable-next-line react-refresh/only-export-components export const useApi = () => { const { hasAuth, authenticatedUser, currentSession, logIn, logOut, posts, newPost, updatePost, deletePost, users, user, createUser, confirmUser, updateUser, updateUserImage, updateUserPermissions, deleteUser, userPosts, } = useContext(ApiContext); if ( logIn && logOut && posts && newPost && updatePost && deletePost && users && user && createUser && confirmUser && updateUser && updateUserImage && updateUserPermissions && deleteUser && userPosts ) { return { hasAuth, authenticatedUser, currentSession, logIn, logOut, posts, newPost, updatePost, deletePost, users, user, createUser, confirmUser, updateUser, updateUserImage, updateUserPermissions, deleteUser, userPosts, }; } throw new Error("Couldn't find context. Is your component inside an ApiProvider?"); }; export const ApiProvider: FC>> = ({ children }) => { const [hasAuth, setHasAuth] = useState(false); const [authenticatedUser, setAuthenticatedUser] = useState(); const [currentSession, setCurrentSession] = useGuestBookStore((state) => [ state.currentSession, state.setCurrentSession, ]); const token = useRef(); useEffect(() => { if (currentSession[0] && !token.current) { token.current = currentSession[0]; refresh(); } }, [currentSession]); //eslint-disable-line react-hooks/exhaustive-deps const logIn = async (email: string, password: string): Promise => { const { user, token: _token, refreshToken } = await (await post('login', { email, password })).json(); setAuthenticatedUser(user); setCurrentSession([_token, refreshToken]); setHasAuth(true); token.current = _token; }; const logOut = async (): Promise => { try { if (token.current) return await (await reAuth(() => postAuth('logout'))).json(); return true; } catch { return false; } finally { setAuthenticatedUser(undefined); setCurrentSession([undefined, undefined]); setHasAuth(false); token.current = undefined; } }; const posts = async (page?: number): Promise => { const url = `posts?p=${page ?? 0}&l=${POST_LIMIT}`; if (hasAuth) return await (await reAuth(() => getAuth(url))).json(); return await (await get(url)).json(); }; const newPost = async (data: PostCreate): Promise => { return await ( await reAuth(() => postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record)) ).json(); }; const updatePost = async (data: PostUpdate, id: number): Promise => { return await (await reAuth(() => patchAuth(`posts/${id}`, data as Record))).json(); }; const deletePost = async (id: number): Promise => { return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json(); }; const users = async (page?: number): Promise => { const url = `users?p=${page ?? 0}&l=${POST_LIMIT}`; return await (await reAuth(() => getAuth(url))).json(); }; const user = async (id?: number): Promise => { return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json(); }; const createUser = async (data: UserCreate): Promise => { return await (await post(`register`, data as unknown as Record)).json(); }; const confirmUser = async (code: string): Promise => { return await (await patch(`register`, { code })).json(); }; const updateUser = async (data: UserUpdate, id?: number): Promise => { const _user = await ( await reAuth(() => patchAuth(`users/${id ?? 'self'}`, data as Record)) ).json(); setAuthenticatedUser(_user); return _user; }; const updateUserImage = async (data: UserImageUpdate, id?: number): Promise => { const formData = new FormData(); if (data.image) formData.append('image', data.image); if (!data.image && data.predefined) formData.append('predefined', data.predefined); const _user = await (await reAuth(() => postAuthRaw(`users/${id ?? 'self'}/image`, formData))).json(); setAuthenticatedUser(_user); return _user; }; const updateUserPermissions = async (data: UserPermissionsUpdate, id: number): Promise => { const _user = await ( await reAuth(() => patchAuth(`users/${id}/permissions`, data as Record)) ).json(); return _user; }; const userPosts = async (id?: number): Promise => { return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json(); }; const deleteUser = async (id: number): Promise => { return await (await reAuth(() => _delete(`users/${id}?l=${POST_LIMIT}`))).json(); }; /* IMPL */ const post = async (endpoint: string, body?: Record, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'post', headers, body: JSON.stringify(body), }); if (response.ok) return response; throw await response.json(); }; const postAuth = async (endpoint: string, body?: Record, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'post', headers: { token: token.current ?? '', ...headers }, body: JSON.stringify(body), }); if (response.ok) return response; throw await response.json(); }; const postAuthRaw = async (endpoint: string, body?: FormData, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'post', headers: { token: token.current ?? '', ...headers }, body, }); if (response.ok) return response; throw await response.json(); }; const get = async (endpoint: string, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'get', headers, }); if (response.ok) return response; throw await response.json(); }; const getAuth = async (endpoint: string, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'get', headers: { token: token.current ?? '', ...headers }, }); if (response.ok) return response; throw await response.json(); }; const _delete = async (endpoint: string, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'delete', headers: { token: token.current ?? '', ...headers }, }); if (response.ok) return response; throw await response.json(); }; const patch = async (endpoint: string, body?: Record, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'patch', headers: headers, body: JSON.stringify(body), }); if (response.ok) return response; throw await response.json(); }; const patchAuth = async (endpoint: string, body?: Record, headers?: HeadersInit) => { const response = await fetch(`${BASE}${endpoint}`, { mode: 'cors', method: 'patch', headers: { token: token.current ?? '', ...headers }, body: JSON.stringify(body), }); if (response.ok) return response; throw await response.json(); }; const reAuth = async (callback: () => Promise): Promise => { try { console.log('[REAUTH] fetching'); const ret = await callback(); return ret; //eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { console.log('[REAUTH] failed once', error); if (error.code !== ERRORS.UNAUTHORIZED) throw error; try { console.log('[REAUTH] failed due to authentication, try refreshing session'); // REAUTH await refresh(); // DO AGAIN console.log('[REAUTH] refreshed, fetching again'); const ret = await callback(); return ret; } catch (_error) { console.log('[REAUTH] terminating session', _error); setAuthenticatedUser(undefined); setHasAuth(false); setCurrentSession([undefined, undefined]); token.current = undefined; throw _error; } } }; const refresh = async () => { const { user: _user, token: _token, refreshToken, } = await (await postAuth('refresh', { refreshToken: currentSession[1] ?? 'INVALID DEFAULT' })).json(); setAuthenticatedUser(_user); setCurrentSession([_token, refreshToken]); setHasAuth(true); token.current = _token; }; return ( {children} ); }; export default ApiProvider;