From 5e1c672cc70e22a0612f6ff8c5db62cbe7935d7d Mon Sep 17 00:00:00 2001 From: Lowell Torola <44183219+lowtorola@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:47:25 -0400 Subject: [PATCH] Api Cleanup (#664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use functions for api, not static class methods 👍 --- frontend2/.eslintrc.js | 3 +- frontend2/.prettierignore | 3 + frontend2/src/components/BattlecodeTable.tsx | 16 +- frontend2/src/components/elements/Button.tsx | 8 +- frontend2/src/utils/api.ts | 1318 ++++++++---------- frontend2/src/utils/auth.ts | 98 ++ frontend2/src/views/Rankings.tsx | 2 +- 7 files changed, 728 insertions(+), 720 deletions(-) create mode 100644 frontend2/src/utils/auth.ts diff --git a/frontend2/.eslintrc.js b/frontend2/.eslintrc.js index d919a4a37..04a28c0ce 100644 --- a/frontend2/.eslintrc.js +++ b/frontend2/.eslintrc.js @@ -18,7 +18,7 @@ module.exports = { "*.json", ".eslintrc.js", "tailwind.config.js", - "**/types/**", + "src/utils/types", ], parserOptions: { project: "tsconfig.json", @@ -28,7 +28,6 @@ module.exports = { }, plugins: ["react"], rules: { - indent: ["error", 2], semi: ["error", "always"], // require semicolons ending statements }, settings: { diff --git a/frontend2/.prettierignore b/frontend2/.prettierignore index 3fc0f4ade..f94045d21 100644 --- a/frontend2/.prettierignore +++ b/frontend2/.prettierignore @@ -7,6 +7,9 @@ # production /build +# auto-generated type +src/utils/types + /coverage package-lock.json diff --git a/frontend2/src/components/BattlecodeTable.tsx b/frontend2/src/components/BattlecodeTable.tsx index 5785bac61..026df510d 100644 --- a/frontend2/src/components/BattlecodeTable.tsx +++ b/frontend2/src/components/BattlecodeTable.tsx @@ -49,15 +49,15 @@ function BattlecodeTable({ className={ idx % 2 === 0 ? `bg-white border-b ${ - onRowClick !== undefined - ? "cursor-pointer hover:bg-gray-100 hover:text-gray-700" - : "" - }}` + onRowClick !== undefined + ? "cursor-pointer hover:bg-gray-100 hover:text-gray-700" + : "" + }}` : `bg-gray-50 border-b ${ - onRowClick !== undefined - ? "cursor-pointer hover:bg-gray-100 hover:text-gray-700" - : "" - }` + onRowClick !== undefined + ? "cursor-pointer hover:bg-gray-100 hover:text-gray-700" + : "" + }` } > {columns.map((col, idx) => ( diff --git a/frontend2/src/components/elements/Button.tsx b/frontend2/src/components/elements/Button.tsx index bb6793a68..3f7bb2820 100644 --- a/frontend2/src/components/elements/Button.tsx +++ b/frontend2/src/components/elements/Button.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> { variant?: string; @@ -7,12 +7,10 @@ interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> { const variants: Record = { "": "bg-gray-50 text-gray-900 hover:bg-gray-100 ring-gray-300 ring-1 ring-inset", - "dark": "bg-gray-700 text-gray-50 hover:bg-gray-800" + dark: "bg-gray-700 text-gray-50 hover:bg-gray-800", }; -const Button: React.FC = ( - { variant, label, ...rest } -) => { +const Button: React.FC = ({ variant, label, ...rest }) => { variant = variant ?? ""; const variantStyle = variants[variant]; return ( diff --git a/frontend2/src/utils/api.ts b/frontend2/src/utils/api.ts index 5561ca02f..8febbda1f 100644 --- a/frontend2/src/utils/api.ts +++ b/frontend2/src/utils/api.ts @@ -9,713 +9,623 @@ const baseUrl = process.env.REACT_APP_BACKEND_URL ?? "http://localhost:8000"; // TODO: how does url work? @index.tsx? // This is an instance of the auto-generated API class. -// The "ApiApi" class should not be imported/used anywhere but this file! +// The "ApiApi" class should not be imported/used anywhere but this file and auth.ts! const API = new ApiApi(baseUrl); -export class Api { - // -- TOKEN HANDLING --// - - /** - * Takes a set of user credentials and returns an access and refresh JSON web token pair to prove the authentication of those credentials. - * - TODO: Rework cookie policy - https://github.com/battlecode/galaxy/issues/647 - * @param credentials The user's credentials. - */ - public static getApiTokens = async ( - credentials: models.TokenObtainPair - ): Promise => { - return (await API.apiTokenCreate(credentials)).body; - }; - - /** - * Checks whether the current access token in the browser's cookies is valid. - * Returns a promise that resolves to true if the token is valid, and false otherwise. - */ - public static verifyCurrentToken = async (): Promise => { - const accessToken = Cookies.get("access"); - if (accessToken !== undefined) { - return ( - (await API.apiTokenVerifyCreate({ token: accessToken })).response - .status === 200 - ); - } else { - return false; - } - }; - - // -- EPISODES --// - /** - * Get all maps for the provided episode. - * @param episodeId The current episode's ID. - */ - public static getAllMaps = async ( - episodeId: string - ): Promise => { +// -- TOKEN HANDLING --// + +/** + * Takes a set of user credentials and returns an access and refresh JSON web token pair to prove the authentication of those credentials. + * - TODO: Rework cookie policy - https://github.com/battlecode/galaxy/issues/647 + * @param credentials The user's credentials. + */ +export const getApiTokens = async ( + credentials: models.TokenObtainPair +): Promise => { + return (await API.apiTokenCreate(credentials)).body; +}; + +/** + * Checks whether the current access token in the browser's cookies is valid. + * Returns a promise that resolves to true if the token is valid, and false otherwise. + */ +export const verifyCurrentToken = async (): Promise => { + const accessToken = Cookies.get("access"); + if (accessToken !== undefined) { return ( - (await $.get(`${baseUrl}/api/episode/${episodeId}/map/`)) ?? - ([] as models.ModelMap[]) + (await API.apiTokenVerifyCreate({ token: accessToken })).response + .status === 200 ); - }; - - // -- TEAMS --// - - /** - * Creates a new team. - * @param teamName The name of the team. - */ - public static createTeam = async ( - episodeId: string, - teamName: string - ): Promise => { - // build default object... why? I couldn't tell you - const teamCreate = { - id: -1, + } else { + return false; + } +}; + +// -- EPISODES --// +/** + * Get all maps for the provided episode. + * @param episodeId The current episode's ID. + */ +export const getAllMaps = async ( + episodeId: string +): Promise => { + return ( + ((await $.get( + `${baseUrl}/api/episode/${episodeId}/map/` + )) as models.ModelMap[]) ?? [] + ); +}; + +// -- TEAMS --// + +/** + * Creates a new team. + * @param teamName The name of the team. + */ +export const createTeam = async ( + episodeId: string, + teamName: string +): Promise => { + // build default object... why? I couldn't tell you + const teamCreate = { + id: -1, + episodeId, + name: teamName, + members: [], + joinKey: "", + status: models.Status526Enum.R, + }; + + return (await API.apiTeamTCreate(episodeId, teamCreate)).body; +}; + +/** + * Join the team with the given join key & name. + * @param episodeId The current episode's ID. + * @param teamName The team's name. + * @param joinKey The team's join key. + */ +export const joinTeam = async ( + episodeId: string, + teamName: string, + joinKey: string +): Promise => { + const teamInfo = { + name: teamName, + joinKey, + }; + await API.apiTeamTJoinCreate(episodeId, teamInfo); +}; + +/** + * Leave the user's current team. + * @param episodeId The current episode's ID. + */ +export const leaveTeam = async (episodeId: string): Promise => { + await API.apiTeamTLeaveCreate(episodeId); +}; + +/** + * Updates the current user's team's join key. + * @param episodeId The current episode's ID. + * @param joinKey The new team join key. + */ +export const updateUserTeamCode = async ( + episodeId: string, + joinKey: string +): Promise => { + return (await API.apiTeamTMePartialUpdate(episodeId, { joinKey })).body; +}; + +// -- TEAM STATS --// + +// TODO: implement rankings history +// /** +// * Get the Mu history of the given team. +// * @param teamId The team's ID. +// */ +// export const getTeamMuHistoryByTeam = async (teamId: number) => { +// return await $.get(`${baseUrl}/api/${LEAGUE}/team/${teamId}/history/`); +// }; + +/** + * getTeamMuHistoryByTeam + */ + +/** + * getTeamWinStatsByTeam + */ + +/** + * getUserTeamWinStats + */ + +/** + * getTeamInfoByTeam + */ + +/** + * getTeamRankingByTeam + */ + +// -- SEARCHING --// + +/** + * Search team, ordering the result by ranking. + * @param episodeId The current episode's ID. + * @param searchQuery The search query. + * @param requireActiveSubmission Whether to require an active submission. + * @param page The page number. + */ +export const searchTeams = async ( + episodeId: string, + searchQuery: string, + requireActiveSubmission: boolean, + page?: number +): Promise => { + const apiURL = `${baseUrl}/api/team/${episodeId}/t`; + const encQuery = encodeURIComponent(searchQuery); + const teamUrl = + `${apiURL}/?ordering=-rating,name&search=${encQuery}&page=${page ?? 1}` + + (requireActiveSubmission ? `&has_active_submission=true` : ``); + return (await $.get(teamUrl)) as models.PaginatedTeamPublicList; +}; + +// -- GENERAL INFO --// + +/** + * Get the current episode's info. + * @param episodeId The current episode's ID. + */ +export const getEpisodeInfo = async ( + episodeId: string +): Promise => { + return (await API.apiEpisodeERetrieve(episodeId)).body; +}; + +/** + * Get updates about the current league. + * TODO: No idea how this is supposed to work! + */ +// export const getUpdates = async (): Promise => { +// return await $.get(`${baseUrl}/api/league/${LEAGUE}/`, (data) => { +// for (let i = 0; i < data.updates.length; i++) { +// const d = new Date(data.updates[i].time); +// data.updates[i].dateObj = d; +// data.updates[i].date = d.toLocaleDateString(); +// data.updates[i].time = d.toLocaleTimeString(); +// } +// }); +// }; + +// -- SUBMISSIONS --// + +/** + * Uploads a new submission to the Google Cloud Storage bucket. + * @param episodeId The current episode's ID. + * @param submission The submission's info. + */ +export const uploadSubmission = async ( + episodeId: string, + submission: { + file: File; + packageName: string; + description: string; + } +): Promise => { + const fileData = new FormData(); + fileData.append("source_code", submission.file); + fileData.append("package", submission.packageName); + fileData.append("description", submission.description); + await $.ajax({ + url: `${baseUrl}/api/episode/${episodeId}/submission/`, + type: "POST", + data: fileData, + dataType: "json", + processData: false, + contentType: false, + }); +}; + +/** + * Download a submission from the Google Cloud Storage bucket. + * @param episodeId The current episode's ID. + * @param submissionId The submission's ID. + */ +export const downloadSubmission = async ( + episodeId: string, + submissionId: number +): Promise => { + const url: string = ( + await API.apiCompeteSubmissionDownloadRetrieve( episodeId, - name: teamName, - members: [], - joinKey: "", - status: models.Status526Enum.R, - }; - - return (await API.apiTeamTCreate(episodeId, teamCreate)).body; - }; - - /** - * Join the team with the given join key & name. - * @param episodeId The current episode's ID. - * @param teamName The team's name. - * @param joinKey The team's join key. - */ - public static joinTeam = async ( - episodeId: string, - teamName: string, - joinKey: string - ): Promise => { - const teamInfo = { - name: teamName, - joinKey, - }; - await API.apiTeamTJoinCreate(episodeId, teamInfo); - }; - - /** - * Leave the user's current team. - * @param episodeId The current episode's ID. - */ - public static leaveTeam = async (episodeId: string): Promise => { - await API.apiTeamTLeaveCreate(episodeId); - }; - - /** - * Updates the current user's team's join key. - * @param episodeId The current episode's ID. - * @param joinKey The new team join key. - */ - public static updateUserTeamCode = async ( - episodeId: string, - joinKey: string - ): Promise => { - return (await API.apiTeamTMePartialUpdate(episodeId, { joinKey })).body; - }; - - // -- TEAM STATS --// - - // TODO: implement rankings history - // /** - // * Get the Mu history of the given team. - // * @param teamId The team's ID. - // */ - // public static getTeamMuHistoryByTeam = async (teamId: number) => { - // return await $.get(`${baseUrl}/api/${LEAGUE}/team/${teamId}/history/`); - // }; - - /** - * getTeamMuHistoryByTeam - */ - - /** - * getTeamWinStatsByTeam - */ - - /** - * getUserTeamWinStats - */ - - /** - * getTeamInfoByTeam - */ - - /** - * getTeamRankingByTeam - */ - - // -- SEARCHING --// - - /** - * Search team, ordering the result by ranking. - * @param episodeId The current episode's ID. - * @param searchQuery The search query. - * @param requireActiveSubmission Whether to require an active submission. - * @param page The page number. - */ - public static searchTeams = async ( - episodeId: string, - searchQuery: string, - requireActiveSubmission: boolean, - page?: number - ): Promise => { - const apiURL = `${baseUrl}/api/team/${episodeId}/t`; - const encQuery = encodeURIComponent(searchQuery); - const teamUrl = - `${apiURL}/?ordering=-rating,name&search=${encQuery}&page=${page ?? 1}` + - (requireActiveSubmission ? `&has_active_submission=true` : ``); - return await $.get(teamUrl); - }; - - // -- GENERAL INFO --// - - /** - * Get the current episode's info. - * @param episodeId The current episode's ID. - */ - public static getEpisodeInfo = async ( - episodeId: string - ): Promise => { - return (await API.apiEpisodeERetrieve(episodeId)).body; - }; - - /** - * Get updates about the current league. - * TODO: No idea how this is supposed to work! - */ - // public static getUpdates = async (): Promise => { - // return await $.get(`${baseUrl}/api/league/${LEAGUE}/`, (data) => { - // for (let i = 0; i < data.updates.length; i++) { - // const d = new Date(data.updates[i].time); - // data.updates[i].dateObj = d; - // data.updates[i].date = d.toLocaleDateString(); - // data.updates[i].time = d.toLocaleTimeString(); - // } - // }); - // }; - - // -- SUBMISSIONS --// - - /** - * Uploads a new submission to the Google Cloud Storage bucket. - * @param episodeId The current episode's ID. - * @param submission The submission's info. - */ - public static uploadSubmission = async ( - episodeId: string, - submission: { - file: File; - packageName: string; - description: string; - } - ): Promise => { - const fileData = new FormData(); - fileData.append("source_code", submission.file); - fileData.append("package", submission.packageName); - fileData.append("description", submission.description); - await $.ajax({ - url: `${baseUrl}/api/episode/${episodeId}/submission/`, - type: "POST", - data: fileData, - dataType: "json", - processData: false, - contentType: false, - }); - }; - - /** - * Download a submission from the Google Cloud Storage bucket. - * @param episodeId The current episode's ID. - * @param submissionId The submission's ID. - */ - public static downloadSubmission = async ( - episodeId: string, - submissionId: number - ): Promise => { - const url: string = ( - await API.apiCompeteSubmissionDownloadRetrieve( - episodeId, - submissionId.toString() - ) - ).body.url; - - await fetch(url) - .then(async (response) => await response.blob()) - .then((blob) => { - // code to download the file given by the URL - const objUrl = window.URL.createObjectURL(blob); - const aHelper = document.createElement("a"); - aHelper.style.display = "none"; - aHelper.href = objUrl; - aHelper.download = `battlecode_source_${submissionId}.zip`; - document.body.appendChild(aHelper); - aHelper.click(); - window.URL.revokeObjectURL(objUrl); - }); - }; - - /** - * Get all submissions. - * @param episodeId The current episode's ID. - * @param page The page number. - */ - public static getAllSubmissions = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiCompeteSubmissionList(episodeId, page)).body; - }; - - /** - * Get all tournament Submissions for the currently logged in user's team. - * @param episodeId The current episode's ID. - * @param page The page number. - */ - public static getAllUserTournamentSubmissions = async ( - episodeId: string, - page?: number - ): Promise => { - const res = await $.get( - `${baseUrl}/api/compete/${episodeId}/submission/tournament/?page=${ - page ?? 1 - }` - ); - return { - count: parseInt(res.length ?? "0"), - results: res ?? [], - }; - }; - - // -- USERS --// - - /** - * Create a new user. - * @param user The user's info. - */ - public static createUser = async ( - user: models.UserCreate - ): Promise => { - return (await API.apiUserUCreate(user)).body; - }; - - /** - * Get a user's profile. - * @param userId The user's ID. - */ - public static getUserProfileByUser = async ( - userId: number - ): Promise => { - return (await API.apiUserURetrieve(userId)).body; - }; - - /** - * Get the currently logged in user's profile. - */ - public static getUserUserProfile = async (): Promise => { - return (await API.apiUserUMeRetrieve()).body; - }; - - /** - * Get all teams associated with a user. - * @param userId The user's ID. - */ - public static getTeamsByUser = async ( - userId: number - ): Promise => { - return (await API.apiUserUTeamsRetrieve(userId)).body; - }; - - /** - * Update the currently logged in user's info. - */ - public static updateUser = async ( - user: models.PatchedUserPrivate - ): Promise => { - await API.apiUserUMePartialUpdate(user); - }; - - // -- AVATARS/RESUMES/REPORTS --// - - /** - * Upload a new avatar for the currently logged in user. - * @param avatarFile The avatar file. - */ - public static avatarUpload = async (avatarFile: File): Promise => { - const data = new FormData(); - data.append("avatar", avatarFile); - await $.ajax({ - url: `${baseUrl}/api/user/u/avatar/`, - type: "POST", - data, - dataType: "json", - processData: false, - contentType: false, - }); - }; - - /** - * Upload a new avatar for the currently logged in user's team. - * @param episodeId The current episode's ID. - * @param avatarFile The avatar file. - */ - public static teamAvatarUpload = async ( - episodeId: string, - avatarFile: File - ): Promise => { - const data = new FormData(); - data.append("avatar", avatarFile); - await $.ajax({ - url: `${baseUrl}/api/team/${episodeId}/t/avatar/`, - type: "POST", - data, - dataType: "json", - processData: false, - contentType: false, - }); - }; - - /** - * Upload a resume for the currently logged in user. - * @param resumeFile The resume file. - */ - public static resumeUpload = async (resumeFile: File): Promise => { - const data = new FormData(); - data.append("resume", resumeFile); - await $.ajax({ - url: `${baseUrl}/api/user/u/resume/`, - type: "PUT", - data, - dataType: "json", - processData: false, - contentType: false, + submissionId.toString() + ) + ).body.url; + + await fetch(url) + .then(async (response) => await response.blob()) + .then((blob) => { + // code to download the file given by the URL + const objUrl = window.URL.createObjectURL(blob); + const aHelper = document.createElement("a"); + aHelper.style.display = "none"; + aHelper.href = objUrl; + aHelper.download = `battlecode_source_${submissionId}.zip`; + document.body.appendChild(aHelper); + aHelper.click(); + window.URL.revokeObjectURL(objUrl); }); - }; - - /** - * Download the resume of the currently logged in user. - */ - public static downloadResume = async (): Promise => { - await $.ajax({ - url: `${baseUrl}/api/user/u/resume/`, - type: "GET", - }).done((data) => { - const blob = new Blob([data], { type: "application/pdf" }); - const url = window.URL.createObjectURL(blob); - // See https://stackoverflow.com/a/9970672 for file download logic - const a = document.createElement("a"); - a.style.display = "none"; - a.href = url; - a.download = "resume.pdf"; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - }); - }; - - /** - * Upload a new report for the currently logged in user's team. - * @param episodeId The current episode's ID. - * @param reportFile The report file. - */ - public static uploadUserTeamReport = async ( - episodeId: string, - reportFile: File - ): Promise => { - const data = new FormData(); - data.append("report", reportFile); - await $.ajax({ - url: `${baseUrl}/api/team/${episodeId}/requirement/report/`, - type: "PUT", - data, - dataType: "json", - processData: false, - contentType: false, - }); - }; - - // -- SCRIMMAGES/MATCHES --// - - /** - * Accept a scrimmage invitation. - * @param episodeId The current episode's ID. - * @param scrimmageId The scrimmage's ID to accept. - */ - public static acceptScrimmage = async ( - episodeId: string, - scrimmageId: number - ): Promise => { - const scrimId = scrimmageId.toString(); - await API.apiCompeteRequestAcceptCreate(episodeId, scrimId); - }; - - /** - * Reject a scrimmage invitation. - * @param episodeId The current episode's ID. - * @param scrimmageId The scrimmage's ID to reject. - */ - public static rejectScrimmage = async ( - episodeId: string, - scrimmageId: number - ): Promise => { - const scrimId = scrimmageId.toString(); - await API.apiCompeteRequestRejectCreate(episodeId, scrimId); - }; - - /** - * Get all of the currently logged in user's incoming scrimmage requests. - * @param episodeId The current episode's ID. - */ - public static getUserScrimmagesInbox = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiCompeteRequestInboxList(episodeId, page)).body; - }; - - /** - * Get all of the currently logged in user's outgoing scrimmage requests. - * @param episodeId The current episode's ID. - */ - public static getUserScrimmagesOutbox = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiCompeteRequestOutboxList(episodeId, page)).body; - }; - - /** - * Request a scrimmage with a team. - * @param episodeId The current episode's ID. - * @param request The scrimmage request body. - */ - public static requestScrimmage = async ( - episodeId: string, - request: { - isRanked: boolean; - requestedTo: number; - playerOrder: models.PlayerOrderEnum; - mapNames: string[]; - } - ): Promise => { - // Once again, the important values are params, we can just throw in the rest here to make the type happy - const scrimRequest: models.ScrimmageRequest = { - ...request, - id: -1, - episode: "", - created: "", - status: models.ScrimmageRequestStatusEnum.P, - requestedBy: -1, - requestedByName: "", - requestedByRating: -1, - requestedToName: "", - requestedToRating: -1, - maps: [], - }; - await API.apiCompeteRequestCreate(episodeId, scrimRequest); - }; - - /** - * Get all of the scrimmages that the currently logged in user's team has played. - * @param episodeId The current episode's ID. - * @param page The page of scrimmages to get. - */ - public static getUserScrimmages = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiCompeteMatchScrimmageList(episodeId, page)).body; - }; - - /** - * Get all of the scrimmages that a given team has played. - * @param episodeId The current episode's ID. - * @param teamId The team's ID. - * @param page The page of scrimmages to get. - */ - public static getScrimmagesByTeam = async ( - episodeId: string, - teamId: number, - page?: number - ): Promise => { - return (await API.apiCompeteMatchScrimmageList(episodeId, teamId, page)) - .body; - }; - - /** - * Get all of the tournament matches that the given team has played. - * Can be optionally filtered by tournament and round. - * @param episodeId The current episode's ID. - * @param teamId The team's ID. - * @param tournamentId The tournament's ID. - * @param roundId The tournament round's ID. - * @param page The page of matches to get. - */ - public static getMatchesByTeam = async ( - episodeId: string, - teamId: number, - tournamentId?: string, - roundId?: number, - page?: number - ): Promise => { - return ( - await API.apiCompeteMatchTournamentList( - episodeId, - page, - roundId, - teamId, - tournamentId - ) - ).body; - }; - - /** - * Get all of the tournament matches played in the given episode. - * @param episodeId The current episode's ID. - * @param page The page of matches to get. - */ - public static getAllMatches = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiCompeteMatchList(episodeId, page)).body; - }; - - /** - * Get all of the scrimmages played in the given episode. - * @param episodeId The current episode's ID. - * @param page The page of scrimmages to get. - */ - public static getAllScrimmages = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiCompeteMatchScrimmageList(episodeId, page)).body; - }; - - /** - * Get all of the tournament matches the currently logged in user's team has played. - * @param episodeId The current episode's ID. - * @param tournamentId The tournament's ID. - */ - public static getUserMatches = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiCompeteMatchList(episodeId, page)).body; - }; - - // -- TOURNAMENTS --// - /** - * Get the next tournament occurring during the given episode, as ordered by submission freeze time. - * @param episodeId The current episode's ID. - */ - public static getNextTournament = async ( - episodeId: string - ): Promise => { - return (await API.apiEpisodeTournamentNextRetrieve(episodeId)).body; - }; - - /** - * Get all of the tournaments occurring during the given episode. - * @param episodeId The current episode's ID. - * @param page The page of tournaments to get. - */ - public static getAllTournaments = async ( - episodeId: string, - page?: number - ): Promise => { - return (await API.apiEpisodeTournamentList(episodeId, page)).body; - }; -} - -/** This class contains all frontend authentication functions. Responsible for interacting with Cookies and expiring/setting JWT tokens. */ -export class Auth { - /** - * Clear the access and refresh tokens from the browser's cookies. - */ - public static logout = (): void => { - Cookies.set("access", ""); - Cookies.set("refresh", ""); - Auth.setLoginHeader(); - window.location.replace("/"); - }; - - /** - * Set the access and refresh tokens in the browser's cookies. - * @param username The username of the user. - * @param password The password of the user. - */ - public static login = async ( - username: string, - password: string - ): Promise => { - const credentials = { - username, - password, - access: "", - refresh: "", - }; - - const res = await Api.getApiTokens(credentials); - - Cookies.set("access", res.access); - Cookies.set("refresh", res.refresh); - }; - - /** - * Set authorization header based on the current cookie state, which is provided by - * default for all subsequent requests. The header is a JWT token: see - * https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html - */ - public static setLoginHeader = (): void => { - const accessToken = Cookies.get("access"); - if (accessToken !== undefined) { - $.ajaxSetup({ - headers: { Authorization: `Bearer ${accessToken}` }, - }); - } - }; - - /** - * Checks whether the currently held JWT access token is still valid (by posting it to the verify endpoint), - * hence whether or not the frontend still has logged-in access. - * @returns true or false - * Callers of this method should check this, before rendering their logged-in or un-logged-in versions. - * If not logged in, then api calls will give 403s, and the website will tell you to log in anyways. - */ - public static loginCheck = async (): Promise => { - return await Api.verifyCurrentToken(); - }; - - /** - * Register a new user. - * @param user The user to register. - */ - public static register = async (user: models.UserCreate): Promise => { - await Api.createUser(user); - await Auth.login(user.username, user.password); - }; - - /** - * Confirm resetting a user's password. - * @param password The new password. - * @param token The password reset token. - */ - public static doResetPassword = async ( - password: string, - token: string - ): Promise => { - await API.apiUserPasswordResetConfirmCreate({ password, token }); - }; - - /** - * Request a password reset token to be sent to the provided email. - */ - public static forgotPassword = async (email: string): Promise => { - await API.apiUserPasswordResetCreate({ email }); - }; -} +}; + +/** + * Get all submissions. + * @param episodeId The current episode's ID. + * @param page The page number. + */ +export const getAllSubmissions = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiCompeteSubmissionList(episodeId, page)).body; +}; + +/** + * Get all tournament Submissions for the currently logged in user's team. + * @param episodeId The current episode's ID. + * @param page The page number. + */ +export const getAllUserTournamentSubmissions = async ( + episodeId: string, + page?: number +): Promise => { + const res: models.Submission[] = (await $.get( + `${baseUrl}/api/compete/${episodeId}/submission/tournament/?page=${ + page ?? 1 + }` + )) as unknown as models.Submission[]; + return { + count: res.length, + results: res ?? [], + }; +}; + +// -- USERS --// + +/** + * Create a new user. + * @param user The user's info. + */ +export const createUser = async ( + user: models.UserCreate +): Promise => { + return (await API.apiUserUCreate(user)).body; +}; + +/** + * Get a user's profile. + * @param userId The user's ID. + */ +export const getUserProfileByUser = async ( + userId: number +): Promise => { + return (await API.apiUserURetrieve(userId)).body; +}; + +/** + * Get the currently logged in user's profile. + */ +export const getUserUserProfile = async (): Promise => { + return (await API.apiUserUMeRetrieve()).body; +}; + +/** + * Get all teams associated with a user. + * @param userId The user's ID. + */ +export const getTeamsByUser = async ( + userId: number +): Promise => { + return (await API.apiUserUTeamsRetrieve(userId)).body; +}; + +/** + * Update the currently logged in user's info. + */ +export const updateUser = async ( + user: models.PatchedUserPrivate +): Promise => { + await API.apiUserUMePartialUpdate(user); +}; + +// -- AVATARS/RESUMES/REPORTS --// + +/** + * Upload a new avatar for the currently logged in user. + * @param avatarFile The avatar file. + */ +export const avatarUpload = async (avatarFile: File): Promise => { + const data = new FormData(); + data.append("avatar", avatarFile); + await $.ajax({ + url: `${baseUrl}/api/user/u/avatar/`, + type: "POST", + data, + dataType: "json", + processData: false, + contentType: false, + }); +}; + +/** + * Upload a new avatar for the currently logged in user's team. + * @param episodeId The current episode's ID. + * @param avatarFile The avatar file. + */ +export const teamAvatarUpload = async ( + episodeId: string, + avatarFile: File +): Promise => { + const data = new FormData(); + data.append("avatar", avatarFile); + await $.ajax({ + url: `${baseUrl}/api/team/${episodeId}/t/avatar/`, + type: "POST", + data, + dataType: "json", + processData: false, + contentType: false, + }); +}; + +/** + * Upload a resume for the currently logged in user. + * @param resumeFile The resume file. + */ +export const resumeUpload = async (resumeFile: File): Promise => { + const data = new FormData(); + data.append("resume", resumeFile); + await $.ajax({ + url: `${baseUrl}/api/user/u/resume/`, + type: "PUT", + data, + dataType: "json", + processData: false, + contentType: false, + }); +}; + +/** + * Download the resume of the currently logged in user. + */ +export const downloadResume = async (): Promise => { + await $.ajax({ + url: `${baseUrl}/api/user/u/resume/`, + type: "GET", + }).done((data) => { + const blob = new Blob([data], { type: "application/pdf" }); + const url = window.URL.createObjectURL(blob); + // See https://stackoverflow.com/a/9970672 for file download logic + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = "resume.pdf"; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }); +}; + +/** + * Upload a new report for the currently logged in user's team. + * @param episodeId The current episode's ID. + * @param reportFile The report file. + */ +export const uploadUserTeamReport = async ( + episodeId: string, + reportFile: File +): Promise => { + const data = new FormData(); + data.append("report", reportFile); + await $.ajax({ + url: `${baseUrl}/api/team/${episodeId}/requirement/report/`, + type: "PUT", + data, + dataType: "json", + processData: false, + contentType: false, + }); +}; + +// -- SCRIMMAGES/MATCHES --// + +/** + * Accept a scrimmage invitation. + * @param episodeId The current episode's ID. + * @param scrimmageId The scrimmage's ID to accept. + */ +export const acceptScrimmage = async ( + episodeId: string, + scrimmageId: number +): Promise => { + const scrimId = scrimmageId.toString(); + await API.apiCompeteRequestAcceptCreate(episodeId, scrimId); +}; + +/** + * Reject a scrimmage invitation. + * @param episodeId The current episode's ID. + * @param scrimmageId The scrimmage's ID to reject. + */ +export const rejectScrimmage = async ( + episodeId: string, + scrimmageId: number +): Promise => { + const scrimId = scrimmageId.toString(); + await API.apiCompeteRequestRejectCreate(episodeId, scrimId); +}; + +/** + * Get all of the currently logged in user's incoming scrimmage requests. + * @param episodeId The current episode's ID. + */ +export const getUserScrimmagesInbox = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiCompeteRequestInboxList(episodeId, page)).body; +}; + +/** + * Get all of the currently logged in user's outgoing scrimmage requests. + * @param episodeId The current episode's ID. + */ +export const getUserScrimmagesOutbox = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiCompeteRequestOutboxList(episodeId, page)).body; +}; + +/** + * Request a scrimmage with a team. + * @param episodeId The current episode's ID. + * @param request The scrimmage request body. + */ +export const requestScrimmage = async ( + episodeId: string, + request: { + isRanked: boolean; + requestedTo: number; + playerOrder: models.PlayerOrderEnum; + mapNames: string[]; + } +): Promise => { + // Once again, the important values are params, we can just throw in the rest here to make the type happy + const scrimRequest: models.ScrimmageRequest = { + ...request, + id: -1, + episode: "", + created: "", + status: models.ScrimmageRequestStatusEnum.P, + requestedBy: -1, + requestedByName: "", + requestedByRating: -1, + requestedToName: "", + requestedToRating: -1, + maps: [], + }; + await API.apiCompeteRequestCreate(episodeId, scrimRequest); +}; + +/** + * Get all of the scrimmages that the currently logged in user's team has played. + * @param episodeId The current episode's ID. + * @param page The page of scrimmages to get. + */ +export const getUserScrimmages = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiCompeteMatchScrimmageList(episodeId, page)).body; +}; + +/** + * Get all of the scrimmages that a given team has played. + * @param episodeId The current episode's ID. + * @param teamId The team's ID. + * @param page The page of scrimmages to get. + */ +export const getScrimmagesByTeam = async ( + episodeId: string, + teamId: number, + page?: number +): Promise => { + return (await API.apiCompeteMatchScrimmageList(episodeId, teamId, page)).body; +}; + +/** + * Get all of the tournament matches that the given team has played. + * Can be optionally filtered by tournament and round. + * @param episodeId The current episode's ID. + * @param teamId The team's ID. + * @param tournamentId The tournament's ID. + * @param roundId The tournament round's ID. + * @param page The page of matches to get. + */ +export const getMatchesByTeam = async ( + episodeId: string, + teamId: number, + tournamentId?: string, + roundId?: number, + page?: number +): Promise => { + return ( + await API.apiCompeteMatchTournamentList( + episodeId, + page, + roundId, + teamId, + tournamentId + ) + ).body; +}; + +/** + * Get all of the tournament matches played in the given episode. + * @param episodeId The current episode's ID. + * @param page The page of matches to get. + */ +export const getAllMatches = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiCompeteMatchList(episodeId, page)).body; +}; + +/** + * Get all of the scrimmages played in the given episode. + * @param episodeId The current episode's ID. + * @param page The page of scrimmages to get. + */ +export const getAllScrimmages = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiCompeteMatchScrimmageList(episodeId, page)).body; +}; + +/** + * Get all of the tournament matches the currently logged in user's team has played. + * @param episodeId The current episode's ID. + * @param tournamentId The tournament's ID. + */ +export const getUserMatches = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiCompeteMatchList(episodeId, page)).body; +}; + +// -- TOURNAMENTS --// +/** + * Get the next tournament occurring during the given episode, as ordered by submission freeze time. + * @param episodeId The current episode's ID. + */ +export const getNextTournament = async ( + episodeId: string +): Promise => { + return (await API.apiEpisodeTournamentNextRetrieve(episodeId)).body; +}; + +/** + * Get all of the tournaments occurring during the given episode. + * @param episodeId The current episode's ID. + * @param page The page of tournaments to get. + */ +export const getAllTournaments = async ( + episodeId: string, + page?: number +): Promise => { + return (await API.apiEpisodeTournamentList(episodeId, page)).body; +}; diff --git a/frontend2/src/utils/auth.ts b/frontend2/src/utils/auth.ts new file mode 100644 index 000000000..8f1ffc077 --- /dev/null +++ b/frontend2/src/utils/auth.ts @@ -0,0 +1,98 @@ +import * as Api from "./api"; +import Cookies from "js-cookie"; +import type * as models from "./types/model/models"; +import { ApiApi } from "./types/api/ApiApi"; + +/** This file contains all frontend authentication functions. Responsible for interacting with Cookies and expiring/setting JWT tokens. */ + +// hacky, fall back to localhost for now +const baseUrl = process.env.REACT_APP_BACKEND_URL ?? "http://localhost:8000"; + +// This is an instance of the auto-generated API class. +// The "ApiApi" class should not be imported/used anywhere but this file and auth.ts! +const API = new ApiApi(baseUrl); + +/** + * Clear the access and refresh tokens from the browser's cookies. + */ +export const logout = (): void => { + Cookies.set("access", ""); + Cookies.set("refresh", ""); + setLoginHeader(); + window.location.replace("/"); +}; + +/** + * Set the access and refresh tokens in the browser's cookies. + * @param username The username of the user. + * @param password The password of the user. + */ +export const login = async ( + username: string, + password: string +): Promise => { + const credentials = { + username, + password, + access: "", + refresh: "", + }; + + const res = await Api.getApiTokens(credentials); + + Cookies.set("access", res.access); + Cookies.set("refresh", res.refresh); +}; + +/** + * Set authorization header based on the current cookie state, which is provided by + * default for all subsequent requests. The header is a JWT token: see + * https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html + */ +export const setLoginHeader = (): void => { + const accessToken = Cookies.get("access"); + if (accessToken !== undefined) { + $.ajaxSetup({ + headers: { Authorization: `Bearer ${accessToken}` }, + }); + } +}; + +/** + * Checks whether the currently held JWT access token is still valid (by posting it to the verify endpoint), + * hence whether or not the frontend still has logged-in access. + * @returns true or false + * Callers of this method should check this, before rendering their logged-in or un-logged-in versions. + * If not logged in, then api calls will give 403s, and the website will tell you to log in anyways. + */ +export const loginCheck = async (): Promise => { + return await Api.verifyCurrentToken(); +}; + +/** + * Register a new user. + * @param user The user to register. + */ +export const register = async (user: models.UserCreate): Promise => { + await Api.createUser(user); + await login(user.username, user.password); +}; + +/** + * Confirm resetting a user's password. + * @param password The new password. + * @param token The password reset token. + */ +export const doResetPassword = async ( + password: string, + token: string +): Promise => { + await API.apiUserPasswordResetConfirmCreate({ password, token }); +}; + +/** + * Request a password reset token to be sent to the provided email. + */ +export const forgotPassword = async (email: string): Promise => { + await API.apiUserPasswordResetCreate({ email }); +}; diff --git a/frontend2/src/views/Rankings.tsx b/frontend2/src/views/Rankings.tsx index 85043fe3b..68fd7e59e 100644 --- a/frontend2/src/views/Rankings.tsx +++ b/frontend2/src/views/Rankings.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from "react"; import { EpisodeContext } from "../contexts/EpisodeContext"; -import { Api } from "../utils/api"; +import * as Api from "../utils/api"; import BattlecodeTable from "../components/BattlecodeTable"; import { type PaginatedTeamPublicList } from "../utils/types/model/PaginatedTeamPublicList"; import BattlecodeTableBottomElement from "../components/BattlecodeTableBottomElement";