2024-07-30 21:20:51 +02:00

395 lines
11 KiB
TypeScript

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<void>;
logOut?: () => Promise<boolean>;
posts?: (page?: number) => Promise<PostListNonAuth | PostListAuth>;
newPost?: (data: PostCreate) => Promise<PostNew>;
updatePost?: (data: PostUpdate, id: number) => Promise<PostAuth>;
deletePost?: (id: number) => Promise<PostDelete>;
users?: (page?: number) => Promise<UserList>;
user?: (id?: number) => Promise<User>;
createUser?: (data: UserCreate) => Promise<User>;
confirmUser?: (code: string) => Promise<User>;
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
updateUserPermissions?: (data: UserPermissionsUpdate, id: number) => Promise<User>;
deleteUser?: (id: number) => Promise<UserDelete>;
userPosts?: (id?: number) => Promise<PostListAuth>;
}
const ApiContext = createContext<ApiContext>({
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<PropsWithChildren<Record<string, unknown>>> = ({ children }) => {
const [hasAuth, setHasAuth] = useState(false);
const [authenticatedUser, setAuthenticatedUser] = useState<User>();
const [currentSession, setCurrentSession] = useGuestBookStore((state) => [
state.currentSession,
state.setCurrentSession,
]);
const token = useRef<string | undefined>();
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<void> => {
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<boolean> => {
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<PostListNonAuth | PostListAuth> => {
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<PostNew> => {
return await (
await reAuth(() => postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record<string, unknown>))
).json();
};
const updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
return await (await reAuth(() => patchAuth(`posts/${id}`, data as Record<string, unknown>))).json();
};
const deletePost = async (id: number): Promise<PostDelete> => {
return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json();
};
const users = async (page?: number): Promise<UserList> => {
const url = `users?p=${page ?? 0}&l=${POST_LIMIT}`;
return await (await reAuth(() => getAuth(url))).json();
};
const user = async (id?: number): Promise<User> => {
return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json();
};
const createUser = async (data: UserCreate): Promise<User> => {
return await (await post(`register`, data as unknown as Record<string, unknown>)).json();
};
const confirmUser = async (code: string): Promise<User> => {
return await (await patch(`register`, { code })).json();
};
const updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
const _user = await (
await reAuth(() => patchAuth(`users/${id ?? 'self'}`, data as Record<string, unknown>))
).json();
setAuthenticatedUser(_user);
return _user;
};
const updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
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<User> => {
const _user = await (
await reAuth(() => patchAuth(`users/${id}/permissions`, data as Record<string, unknown>))
).json();
return _user;
};
const userPosts = async (id?: number): Promise<PostListAuth> => {
return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json();
};
const deleteUser = async (id: number): Promise<UserDelete> => {
return await (await reAuth(() => _delete(`users/${id}?l=${POST_LIMIT}`))).json();
};
/* IMPL */
const post = async (endpoint: string, body?: Record<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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 <T,>(callback: () => Promise<T>): Promise<T> => {
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 (
<ApiContext.Provider
value={{
hasAuth,
authenticatedUser,
currentSession,
logIn,
logOut,
posts,
newPost,
updatePost,
deletePost,
users,
user,
createUser,
confirmUser,
updateUser,
updateUserImage,
updateUserPermissions,
deleteUser,
userPosts,
}}
>
{children}
</ApiContext.Provider>
);
};
export default ApiProvider;