Calling Backend API
To call the backend API, you should use the functions defined in client/src/api.ts
. The helper methods defined in this function let you make GET, POST, PUT, and DELETE requests to the backend.
Authentication
To authenticate a request, you will either need an admin password (auth="admin"
) or a judge token (auth="judge"
).
On all judge pages gated behind the judge login, you should check if the judge has logged in. The judge token
cookie will automatically be passed into request as long as the auth
parameter is set to judge
. Here is an example from live.tsx
, where we check for logged in on page load in a useEffect function:
useEffect(() => {
async function fetchData() {
// Check to see if the user is logged in
const loggedInRes = await postRequest<OkResponse>('/judge/auth', 'judge', null);
if (loggedInRes.data?.ok !== 1 && loggedInRes.status === 401) {
console.error(`Judge is not logged in!`);
navigate('/judge/login');
return;
}
if (loggedInRes.status !== 200) {
errorAlert(loggedInRes);
return;
}
// ... other code
}
fetchData();
}, []);
For admin pages gated behind the admin login, you should check if the admin has logged in. The admin admin-pass
token will automatically passed into the request as long as the auth
parameter is set to admin
. The auth check is the same as above, except using /admin/auth
endpoint and 'auth'
as the auth parameter.
The createHeaders
function in client/src/api.ts
shows that admins use Basic Authentication with judges use Bearer Token Authentication with the API:
export function createHeaders(auth: string, json: boolean): Headers {
const headers = new Headers();
if (json) {
headers.append('Content-Type', 'application/json');
}
if (auth === 'admin') {
const cookies = new Cookies();
const pass = cookies.get('admin-pass');
const basicAuth = btoa(`admin:${pass}`);
headers.append('Authorization', `Basic ${basicAuth}`);
} else if (auth === 'judge') {
const cookies = new Cookies();
const token = cookies.get('token');
headers.append('Authorization', `Bearer ${token}`);
}
return headers;
}
Request Body
When using the POST or PUT requests, you can pass a request body into the function:
export async function postRequest<T>(
path: string,
auth: string,
body: any,
form?: boolean // Whether the body is a form data; otherwise it's JSON
): Promise<FetchResponse<T>> {
// ... details hidden
}
This body should be a POJO (plain-old JS object) as it will be cast to a JSON string. However, if you are passing FormData in, make sure to set the form
parameter to true.
Response Type
All API call helper functions are generic functions which returns a FetchResponse<T>
. The FetchResponse object looks like the following:
interface FetchResponse<T> {
status: number;
error: string;
data: T | null;
}
Generally, you should check the response as following (given that res
is the return from an API call):
if (res.status !== 200) {
errorAlert(res);
return;
}
The errorAlert
function is a wrapper for error logging and alerting. This is standardized across the app and should be used everywhere to report an error from the backend.
To retrieve the data from the response, use the data
field. Make sure to cast it to the type you define. Here is an example from client/src/store.tsx
:
const flagsRes = await getRequest<Flag[]>('/admin/flags', 'admin');
if (flagsRes.status !== 200) {
errorAlert(flagsRes);
return;
}
set({ flags: flagsRes.data as Flag[] });