395 lines
11 KiB
TypeScript
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;
|