diff --git a/backend/rbac.json b/backend/rbac.json new file mode 100644 index 000000000..89025538e --- /dev/null +++ b/backend/rbac.json @@ -0,0 +1,40 @@ +{ + "resources": { + "dev-portfolio": { + "read": ["lead", "admin"], + "write": ["dev_lead", "lead", "admin"] + }, + "dev-portfolio-submission": { + "read": ["lead", "dev_lead", "admin"], + "write": ["dev_lead", "admin"] + }, + "profile-image": { + "read": ["lead", "admin"], + "write": ["lead", "admin"] + }, + "member": { + "read": ["lead", "admin"], + "write": ["lead", "admin"] + }, + "member-diff": { + "read": ["lead", "admin"], + "write": ["lead", "admin"] + }, + "shoutout": { + "read": ["lead", "admin"], + "write": ["lead", "admin"] + }, + "team": { + "read": ["lead", "admin"], + "write": ["lead", "admin"] + }, + "team-event": { + "read": ["lead", "ci_lead", "admin"], + "write": ["ci_lead", "admin"] + }, + "team-event-attendance": { + "read": ["lead", "ci_lead", "admin"], + "write": ["ci_lead", "admin"] + } + } +} diff --git a/backend/src/API/devPortfolioAPI.ts b/backend/src/API/devPortfolioAPI.ts index df15f14b7..4aed13e81 100644 --- a/backend/src/API/devPortfolioAPI.ts +++ b/backend/src/API/devPortfolioAPI.ts @@ -1,7 +1,6 @@ import { DateTime } from 'luxon'; import DevPortfolioDao from '../dao/DevPortfolioDao'; -import PermissionsManager from '../utils/permissionsManager'; -import { PermissionError, BadRequestError, NotFoundError } from '../utils/errors'; +import { BadRequestError, NotFoundError } from '../utils/errors'; import { validateSubmission, isWithinDates } from '../utils/githubUtil'; const zonedTime = (timestamp: number, ianatz = 'America/New_York') => @@ -9,14 +8,8 @@ const zonedTime = (timestamp: number, ianatz = 'America/New_York') => export const devPortfolioDao = new DevPortfolioDao(); -export const getAllDevPortfolios = async (user: IdolMember): Promise => { - const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user); - if (!isLeadOrAdmin) - throw new PermissionError( - `User with email ${user.email} does not have permission to view dev portfolios!` - ); - return devPortfolioDao.getAllInstances(); -}; +export const getAllDevPortfolios = async (user: IdolMember): Promise => + devPortfolioDao.getAllInstances(); export const getAllDevPortfolioInfo = async (): Promise => devPortfolioDao.getAllDevPortfolioInfo(); @@ -30,11 +23,6 @@ export const getUsersDevPortfolioSubmissions = async ( ): Promise => devPortfolioDao.getUsersDevPortfolioSubmissions(uuid, user); export const getDevPortfolio = async (uuid: string, user: IdolMember): Promise => { - const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user); - if (!isLeadOrAdmin) - throw new PermissionError( - `User with email ${user.email} does not have permission to view dev portfolios!` - ); const devPortfolio = await devPortfolioDao.getInstance(uuid); if (!devPortfolio) throw new NotFoundError(`Dev portfolio with uuid: ${uuid} does not exist!`); return devPortfolio; @@ -44,12 +32,6 @@ export const createNewDevPortfolio = async ( instance: DevPortfolio, user: IdolMember ): Promise => { - const canCreateDevPortfolio = await PermissionsManager.isLeadOrAdmin(user); - if (!canCreateDevPortfolio) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to create dev portfolio!` - ); - } if ( !instance.name || instance.name.length === 0 || @@ -77,12 +59,6 @@ export const createNewDevPortfolio = async ( }; export const deleteDevPortfolio = async (uuid: string, user: IdolMember): Promise => { - const canDeleteDevPortfolio = await PermissionsManager.isLeadOrAdmin(user); - if (!canDeleteDevPortfolio) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to delete dev portfolio!` - ); - } await devPortfolioDao.deleteInstance(uuid); }; @@ -116,14 +92,6 @@ export const updateSubmissions = async ( updatedSubmissions: DevPortfolioSubmission[], user: IdolMember ): Promise => { - const canChangeSubmission = await PermissionsManager.isLeadOrAdmin(user); - - if (!canChangeSubmission) { - throw new PermissionError( - `User with email ${user.email} does not have permission to update dev portfolio submissions` - ); - } - const devPortfolio = await devPortfolioDao.getInstance(uuid); if (!devPortfolio) { throw new BadRequestError(`Dev portfolio with uuid: ${uuid} does not exist`); @@ -138,12 +106,6 @@ export const updateSubmissions = async ( }; export const regradeSubmissions = async (uuid: string, user: IdolMember): Promise => { - const canRequestRegrade = await PermissionsManager.isLeadOrAdmin(user); - if (!canRequestRegrade) - throw new PermissionError( - `User with email ${user.email} does not have permission to regrade dev portfolio submissions` - ); - const devPortfolio = await devPortfolioDao.getInstance(uuid); if (!devPortfolio) { throw new BadRequestError(`Dev portfolio with uuid: ${uuid} does not exist`); diff --git a/backend/src/API/memberAPI.ts b/backend/src/API/memberAPI.ts index 66978c4a1..1e01d6a96 100644 --- a/backend/src/API/memberAPI.ts +++ b/backend/src/API/memberAPI.ts @@ -1,6 +1,5 @@ import { Request } from 'express'; import MembersDao from '../dao/MembersDao'; -import PermissionsManager from '../utils/permissionsManager'; import { BadRequestError, PermissionError } from '../utils/errors'; import { bucket } from '../firebase'; import { getNetIDFromEmail, computeMembersDiff } from '../utils/memberUtil'; @@ -14,12 +13,6 @@ export const allApprovedMembers = (): Promise => MembersDao.getAllMembers(true); export const setMember = async (body: IdolMember, user: IdolMember): Promise => { - const canEdit = await PermissionsManager.canEditMembers(user); - if (!canEdit) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to edit members!` - ); - } if (!body.email || body.email === '') { throw new BadRequestError("Couldn't edit member with undefined email!"); } @@ -34,21 +27,13 @@ export const updateMember = async ( body: IdolMember, user: IdolMember ): Promise => { - const canEdit = await PermissionsManager.canEditMembers(user); - if (!canEdit && user.email !== body.email) { - // members are able to edit their own information - throw new PermissionError( - `User with email: ${user.email} does not have permission to edit members!` - ); - } if (!body.email || body.email === '') { throw new BadRequestError("Couldn't edit member with undefined email!"); } if ( - !canEdit && - (body.role !== user.role || - body.firstName !== user.firstName || - body.lastName !== user.lastName) + body.role !== user.role || + body.firstName !== user.firstName || + body.lastName !== user.lastName ) { throw new PermissionError( `User with email: ${user.email} does not have permission to edit member name or roles!` @@ -62,12 +47,6 @@ export const updateMember = async ( }; export const deleteMember = async (email: string, user: IdolMember): Promise => { - const canEdit = await PermissionsManager.canEditMembers(user); - if (!canEdit) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to delete members!` - ); - } if (!email || email === '') { throw new BadRequestError("Couldn't delete member with undefined email!"); } @@ -90,12 +69,6 @@ export const deleteImage = async (email: string): Promise => { export const getUserInformationDifference = async ( user: IdolMember ): Promise => { - const canReview = await PermissionsManager.canReviewChanges(user); - if (!canReview) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to review members information diff!` - ); - } const [allApprovedMembersList, allLatestMembersList] = await Promise.all([ allApprovedMembers(), allMembers() @@ -108,12 +81,6 @@ export const reviewUserInformationChange = async ( rejected: readonly string[], user: IdolMember ): Promise => { - const canReview = await PermissionsManager.canReviewChanges(user); - if (!canReview) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to review members information diff!` - ); - } await Promise.all([ MembersDao.approveMemberInformationChanges(approved), MembersDao.revertMemberInformationChanges(rejected) diff --git a/backend/src/API/shoutoutAPI.ts b/backend/src/API/shoutoutAPI.ts index b748d50dd..d79a7318b 100644 --- a/backend/src/API/shoutoutAPI.ts +++ b/backend/src/API/shoutoutAPI.ts @@ -1,4 +1,3 @@ -import PermissionsManager from '../utils/permissionsManager'; import { NotFoundError, PermissionError } from '../utils/errors'; import ShoutoutsDao from '../dao/ShoutoutsDao'; @@ -19,27 +18,13 @@ export const getShoutouts = async ( memberEmail: string, type: 'given' | 'received', user: IdolMember -): Promise => { - const canEdit: boolean = await PermissionsManager.canGetShoutouts(user); - if (!canEdit && memberEmail !== user.email) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to get shoutouts!` - ); - } - return shoutoutsDao.getShoutouts(memberEmail, type); -}; +): Promise => shoutoutsDao.getShoutouts(memberEmail, type); export const hideShoutout = async ( uuid: string, hide: boolean, user: IdolMember ): Promise => { - const canEdit = await PermissionsManager.canHideShoutouts(user); - if (!canEdit) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to hide shoutouts!` - ); - } const shoutout = await shoutoutsDao.getShoutout(uuid); if (!shoutout) throw new NotFoundError(`Shoutout with uuid: ${uuid} does not exist!`); await shoutoutsDao.updateShoutout({ ...shoutout, hidden: hide }); @@ -50,11 +35,5 @@ export const deleteShoutout = async (uuid: string, user: IdolMember): Promise => { - await checkPermissions(user); const octokit = new Octokit({ auth: `token ${process.env.BOT_TOKEN}`, userAgent: 'cornell-dti/idol-backend' @@ -20,7 +18,6 @@ export const requestIDOLPullDispatch = async (user: IdolMember): Promise<{ updat }; export const getIDOLChangesPR = async (user: IdolMember): Promise<{ pr: PRResponse }> => { - await checkPermissions(user); const foundPR = await findBotPR(); return { pr: foundPR }; }; @@ -28,7 +25,6 @@ export const getIDOLChangesPR = async (user: IdolMember): Promise<{ pr: PRRespon export const acceptIDOLChanges = async ( user: IdolMember ): Promise<{ pr: PRResponse; merged: boolean }> => { - await checkPermissions(user); const octokit = new Octokit({ auth: `token ${process.env.BOT_TOKEN}`, userAgent: 'cornell-dti/idol-backend' @@ -62,7 +58,6 @@ export const acceptIDOLChanges = async ( export const rejectIDOLChanges = async ( user: IdolMember ): Promise<{ pr: PRResponse; closed: boolean }> => { - await checkPermissions(user); const foundPR = await findBotPR(); const octokit2 = new Octokit({ auth: `token ${process.env.BOT_2_TOKEN}`, @@ -99,12 +94,3 @@ const findBotPR = async (): Promise => { } return foundPR; }; - -const checkPermissions = async (user: IdolMember): Promise => { - const canEdit = await PermissionsManager.canDeploySite(user); - if (!canEdit) { - throw new PermissionError( - `User with email: ${user.email} does not have permission to trigger site deploys!` - ); - } -}; diff --git a/backend/src/API/teamAPI.ts b/backend/src/API/teamAPI.ts index f0c1645e9..0505a5d45 100644 --- a/backend/src/API/teamAPI.ts +++ b/backend/src/API/teamAPI.ts @@ -1,7 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; -import PermissionsManager from '../utils/permissionsManager'; import { Team } from '../types/DataTypes'; -import { BadRequestError, PermissionError } from '../utils/errors'; +import { BadRequestError } from '../utils/errors'; import MembersDao from '../dao/MembersDao'; const membersDao = new MembersDao(); @@ -93,12 +92,6 @@ const updateFormerMembers = async (team: Team, oldTeam: Team): Promise => }; export const setTeam = async (teamBody: Team, member: IdolMember): Promise => { - const canEdit = await PermissionsManager.canEditTeams(member); - if (!canEdit) { - throw new PermissionError( - `User with email: ${member.email} does not have permission to edit teams!` - ); - } if (teamBody.members.length > 0 && !teamBody.members[0].email) { throw new BadRequestError('Malformed members on POST!'); } @@ -110,12 +103,6 @@ export const deleteTeam = async (teamBody: Team, member: IdolMember): Promise => { - const canEditTeamEvents = await PermissionsManager.canEditTeamEvent(user); - if (!canEditTeamEvents) throw new PermissionError('does not have permissions'); const teamEvents = await TeamEventsDao.getAllTeamEvents(); return Promise.all( teamEvents.map(async (event) => ({ @@ -29,18 +26,15 @@ export const createTeamEvent = async ( teamEventInfo: TeamEventInfo, user: IdolMember ): Promise => { - const canCreateTeamEvent = await PermissionsManager.canEditTeamEvent(user); - if (!canCreateTeamEvent) - throw new PermissionError('does not have permissions to create team event'); await TeamEventsDao.createTeamEvent(teamEventInfo); return teamEventInfo; }; -export const deleteTeamEvent = async (teamEvent: TeamEvent, user: IdolMember): Promise => { - if (!PermissionsManager.canEditTeamEvent(user)) { - throw new PermissionError("You don't have permission to delete a team event!"); - } - const allAttendances = teamEvent.attendees.concat(teamEvent.requests); +export const deleteTeamEvent = async (uuid: string, user: IdolMember): Promise => { + const teamEvent = await TeamEventsDao.getTeamEvent(uuid); + if (!teamEvent) return; + + const allAttendances = await teamEventAttendanceDao.getTeamEventAttendanceByEventId(uuid); await Promise.all( allAttendances.map((attendance) => @@ -54,11 +48,6 @@ export const updateTeamEvent = async ( teamEventInfo: TeamEventInfo, user: IdolMember ): Promise => { - if (!PermissionsManager.canEditTeamEvent(user)) { - throw new PermissionError( - `User with email ${user.email} does not have permissions to update team events` - ); - } const updatedTeamEvent = await TeamEventsDao.updateTeamEvent(teamEventInfo); return updatedTeamEvent; }; @@ -77,12 +66,6 @@ export const requestTeamEventCredit = async ( }; export const getTeamEvent = async (uuid: string, user: IdolMember): Promise => { - const canEditTeamEvents = await PermissionsManager.canEditTeamEvent(user); - if (!canEditTeamEvents) - throw new PermissionError( - `User with email ${user.email} does not have permission to get full team event` - ); - const teamEvent = await TeamEventsDao.getTeamEvent(uuid); return { ...teamEvent, @@ -96,11 +79,6 @@ export const getTeamEvent = async (uuid: string, user: IdolMember): Promise => { - const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user); - if (!isLeadOrAdmin) - throw new PermissionError( - `User with email ${user.email} does not have sufficient permissions to delete all team events.` - ); await TeamEventAttendanceDao.deleteAllTeamEventAttendance(); await TeamEventsDao.deleteAllTeamEvents(); }; @@ -113,22 +91,10 @@ export const updateTeamEventAttendance = async ( teamEventAttendance: TeamEventAttendance, user: IdolMember ): Promise => { - const canEditTeamEvent = await PermissionsManager.canEditTeamEvent(user); - if (!canEditTeamEvent) { - throw new PermissionError( - `User with email ${user.email} does not have permissions to update team events attendance` - ); - } await teamEventAttendanceDao.updateTeamEventAttendance(teamEventAttendance); return teamEventAttendance; }; export const deleteTeamEventAttendance = async (uuid: string, user: IdolMember): Promise => { - const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user); - if (!isLeadOrAdmin) { - throw new PermissionError( - `User with email ${user.email} does not have sufficient permissions to delete team events attendance` - ); - } await teamEventAttendanceDao.deleteTeamEventAttendance(uuid); }; diff --git a/backend/src/API/teamEventsImageAPI.ts b/backend/src/API/teamEventsImageAPI.ts index 61703b558..c5ba0b57b 100644 --- a/backend/src/API/teamEventsImageAPI.ts +++ b/backend/src/API/teamEventsImageAPI.ts @@ -1,5 +1,4 @@ import { bucket } from '../firebase'; -import { getNetIDFromEmail } from '../utils/memberUtil'; import { NotFoundError } from '../utils/errors'; export const setEventProofImage = async (name: string, user: IdolMember): Promise => { @@ -25,46 +24,46 @@ export const getEventProofImage = async (name: string, user: IdolMember): Promis return signedUrl[0]; }; -export const allEventProofImagesForMember = async ( - user: IdolMember -): Promise => { - const netId: string = getNetIDFromEmail(user.email); - const files = await bucket.getFiles({ prefix: `eventProofs/${netId}` }); - const images = await Promise.all( - files[0].map(async (file) => { - const signedURL = await file.getSignedUrl({ - action: 'read', - expires: Date.now() + 15 * 60000 // 15 min - }); - const fileName = await file.getMetadata().then((data) => data[1].body.name); - return { - fileName, - url: signedURL[0] - }; - }) - ); +// export const allEventProofImagesForMember = async ( +// user: IdolMember +// ): Promise => { +// const netId: string = getNetIDFromEmail(user.email); +// const files = await bucket.getFiles({ prefix: `eventProofs/${netId}` }); +// const images = await Promise.all( +// files[0].map(async (file) => { +// const signedURL = await file.getSignedUrl({ +// action: 'read', +// expires: Date.now() + 15 * 60000 // 15 min +// }); +// const fileName = await file.getMetadata().then((data) => data[1].body.name); +// return { +// fileName, +// url: signedURL[0] +// }; +// }) +// ); - images - .filter((image) => image.fileName.length > 'eventProofs/'.length) - .map((image) => ({ - ...image, - fileName: image.fileName.slice(image.fileName.indexOf('/') + 1) - })); +// images +// .filter((image) => image.fileName.length > 'eventProofs/'.length) +// .map((image) => ({ +// ...image, +// fileName: image.fileName.slice(image.fileName.indexOf('/') + 1) +// })); - return images; -}; +// return images; +// }; export const deleteEventProofImage = async (name: string, user: IdolMember): Promise => { const imageFile = bucket.file(`${name}.jpg`); await imageFile.delete(); }; -export const deleteEventProofImagesForMember = async (user: IdolMember): Promise => { - const netId: string = getNetIDFromEmail(user.email); - const files = await bucket.getFiles({ prefix: `eventProofs/${netId}` }); - Promise.all( - files[0].map(async (file) => { - file.delete(); - }) - ); -}; +// const deleteEventProofImagesForMember = async (user: IdolMember): Promise => { +// const netId: string = getNetIDFromEmail(user.email); +// const files = await bucket.getFiles({ prefix: `eventProofs/${netId}` }); +// Promise.all( +// files[0].map(async (file) => { +// file.delete(); +// }) +// ); +// }; diff --git a/backend/src/api.ts b/backend/src/api.ts index 6f5c8e8ed..c33d12a0f 100644 --- a/backend/src/api.ts +++ b/backend/src/api.ts @@ -1,89 +1,21 @@ -import express, { RequestHandler, Request, Response } from 'express'; +import express from 'express'; import serverless from 'serverless-http'; import cors from 'cors'; -import admin from 'firebase-admin'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; -import { app as adminApp, env } from './firebase'; -import PermissionsManager from './utils/permissionsManager'; -import { HandlerError } from './utils/errors'; -import { - acceptIDOLChanges, - getIDOLChangesPR, - rejectIDOLChanges, - requestIDOLPullDispatch -} from './API/siteIntegrationAPI'; -import { sendMail } from './API/mailAPI'; -import MembersDao from './dao/MembersDao'; -import { - allMembers, - allApprovedMembers, - setMember, - deleteMember, - updateMember, - getUserInformationDifference, - reviewUserInformationChange, - getMember -} from './API/memberAPI'; -import { getMemberImage, setMemberImage, allMemberImages } from './API/imageAPI'; -import { allTeams, setTeam, deleteTeam } from './API/teamAPI'; -import { - getAllShoutouts, - getShoutouts, - giveShoutout, - hideShoutout, - deleteShoutout -} from './API/shoutoutAPI'; -import { - allSignInForms, - createSignInForm, - deleteSignInForm, - signIn, - signInFormExists, - signInFormExpired, - getSignInPrompt -} from './API/signInFormAPI'; -import { - createTeamEvent, - deleteTeamEvent, - getAllTeamEventInfo, - getAllTeamEvents, - getTeamEvent, - updateTeamEvent, - clearAllTeamEvents, - requestTeamEventCredit, - getTeamEventAttendanceByUser, - updateTeamEventAttendance, - deleteTeamEventAttendance -} from './API/teamEventsAPI'; -import { - getAllCandidateDeciderInstances, - createNewCandidateDeciderInstance, - toggleCandidateDeciderInstance, - deleteCandidateDeciderInstance, - getCandidateDeciderInstance, - updateCandidateDeciderRating, - updateCandidateDeciderComment -} from './API/candidateDeciderAPI'; -import { - deleteEventProofImage, - getEventProofImage, - setEventProofImage -} from './API/teamEventsImageAPI'; -import { - getAllDevPortfolios, - createNewDevPortfolio, - deleteDevPortfolio, - makeDevPortfolioSubmission, - getDevPortfolio, - getAllDevPortfolioInfo, - getDevPortfolioInfo, - getUsersDevPortfolioSubmissions, - regradeSubmissions, - updateSubmissions -} from './API/devPortfolioAPI'; -import DPSubmissionRequestLogDao from './dao/DPSubmissionRequestLogDao'; +import { env } from './firebase'; import AdminsDao from './dao/AdminsDao'; +import { getMember } from './API/memberAPI'; +import teamRouter from './routers/teamRouter'; +import candidateDeciderRouter from './routers/candidateDeciderRouter'; +import devPortfolioRouter from './routers/devPortfolioRouter'; +import memberImageRouter from './routers/imageRouter'; +import { memberRouter, memberDiffRouter } from './routers/memberRouter'; +import shoutoutRouter from './routers/shoutoutRouter'; +import signInRouter from './routers/signInRouter'; +import siteIntegrationRouter from './routers/siteIntegrationRouter'; +import teamEventRouter from './routers/teamEventRouter'; +import eventProofImageRouter from './routers/teamEventsImageRouter'; // Constants and configurations const app = express(); @@ -128,67 +60,7 @@ app.use( }) ); -const getUserEmailFromRequest = async (request: Request): Promise => { - const idToken = request.headers['auth-token']; - if (typeof idToken !== 'string') return undefined; - const decodedToken = await admin.auth(adminApp).verifyIdToken(idToken); - return decodedToken.email; -}; - -const loginCheckedHandler = - (handler: (req: Request, user: IdolMember) => Promise>): RequestHandler => - async (req: Request, res: Response): Promise => { - const userEmail = await getUserEmailFromRequest(req); - if (userEmail == null) { - res.status(440).json({ error: 'Not logged in!' }); - return; - } - const user = await MembersDao.getCurrentOrPastMemberByEmail(userEmail); - if (!user) { - res.status(401).send({ error: `No user with email: ${userEmail}` }); - return; - } - if (env === 'staging' && !(await PermissionsManager.isAdmin(user))) { - res.status(401).json({ error: 'Only admins users have permismsions to the staging API!' }); - } - try { - res.status(200).send(await handler(req, user)); - } catch (error) { - if (error instanceof HandlerError) { - res.status(error.errorCode).send({ error: error.reason }); - return; - } - res.status(500).send({ error: `Failed to handle the request due to ${error}.` }); - } - }; - -const loginCheckedGet = ( - path: string, - handler: (req: Request, user: IdolMember) => Promise> -) => router.get(path, loginCheckedHandler(handler)); - -const loginCheckedPost = ( - path: string, - handler: (req: Request, user: IdolMember) => Promise> -) => router.post(path, loginCheckedHandler(handler)); - -const loginCheckedDelete = ( - path: string, - handler: (req: Request, user: IdolMember) => Promise> -) => router.delete(path, loginCheckedHandler(handler)); - // Members -router.get('/allMembers', async (_, res) => { - const members = await allMembers(); - res.status(200).json({ members }); -}); -router.get('/allApprovedMembers', async (_, res) => { - const members = await allApprovedMembers(); - res.status(200).json({ members }); -}); -router.get('/membersFromAllSemesters', async (_, res) => { - res.status(200).json(await MembersDao.getMembersFromAllSemesters()); -}); router.get('/hasIDOLAccess/:email', async (req, res) => { const member = await getMember(req.params.email); const adminEmails = await AdminsDao.getAllAdminEmails(); @@ -201,209 +73,19 @@ router.get('/hasIDOLAccess/:email', async (req, res) => { }); }); -loginCheckedPost('/setMember', async (req, user) => ({ - member: await setMember(req.body, user) -})); -loginCheckedDelete('/deleteMember/:email', async (req, user) => { - await deleteMember(req.params.email, user); - return {}; -}); -loginCheckedPost('/updateMember', async (req, user) => ({ - member: await updateMember(req, req.body, user) -})); - -loginCheckedGet('/memberDiffs', async (_, user) => ({ - diffs: await getUserInformationDifference(user) -})); -loginCheckedPost('/reviewMemberDiffs', async (req, user) => ({ - member: await reviewUserInformationChange(req.body.approved, req.body.rejected, user) -})); - -// Teams -loginCheckedGet('/allTeams', async () => ({ teams: await allTeams() })); -loginCheckedPost('/setTeam', async (req, user) => ({ - team: await setTeam(req.body, user) -})); -loginCheckedPost('/deleteTeam', async (req, user) => ({ - team: await deleteTeam(req.body, user) -})); - -// Images -loginCheckedGet('/getMemberImage', async (_, user) => ({ - url: await getMemberImage(user) -})); -loginCheckedGet('/getImageSignedURL', async (_, user) => ({ - url: await setMemberImage(user) -})); -router.get('/allMemberImages', async (_, res) => { - const images = await allMemberImages(); - res.status(200).json({ images }); -}); - -// Shoutouts -loginCheckedGet('/getShoutouts/:email/:type', async (req, user) => ({ - shoutouts: await getShoutouts(req.params.email, req.params.type as 'given' | 'received', user) -})); - -loginCheckedGet('/allShoutouts', async () => ({ - shoutouts: await getAllShoutouts() -})); - -loginCheckedPost('/giveShoutout', async (req, user) => ({ - shoutout: await giveShoutout(req.body, user) -})); - -loginCheckedPost('/hideShoutout', async (req, user) => { - await hideShoutout(req.body.uuid, req.body.hide, user); - return {}; -}); - -loginCheckedPost('/deleteShoutout', async (req, user) => { - await deleteShoutout(req.body.uuid, user); - return {}; -}); - -// Permissions -loginCheckedGet('/isAdmin', async (_, user) => ({ - isAdmin: await PermissionsManager.isAdmin(user) -})); - -// Pull from IDOL -loginCheckedPost('/pullIDOLChanges', (_, user) => requestIDOLPullDispatch(user)); -loginCheckedGet('/getIDOLChangesPR', (_, user) => getIDOLChangesPR(user)); -loginCheckedPost('/acceptIDOLChanges', (_, user) => acceptIDOLChanges(user)); -loginCheckedPost('/rejectIDOLChanges', (_, user) => rejectIDOLChanges(user)); - -// Sign In Form -loginCheckedPost('/signInExists', async (req, _) => ({ - exists: await signInFormExists(req.body.id) -})); -loginCheckedPost('/signInExpired', async (req, _) => ({ - expired: await signInFormExpired(req.body.id) -})); -loginCheckedPost('/signInCreate', async (req, user) => - createSignInForm(req.body.id, req.body.expireAt, req.body.prompt, user) -); -loginCheckedPost('/signInDelete', async (req, user) => { - await deleteSignInForm(req.body.id, user); - return {}; -}); -loginCheckedPost('/signIn', async (req, user) => signIn(req.body.id, req.body.response, user)); -loginCheckedPost('/signInAll', async (_, user) => allSignInForms(user)); -loginCheckedGet('/signInPrompt/:id', async (req, _) => ({ - prompt: await getSignInPrompt(req.params.id) -})); - -// Team Events -loginCheckedPost('/createTeamEvent', async (req, user) => { - await createTeamEvent(req.body, user); - return {}; -}); -loginCheckedGet('/getTeamEvent/:uuid', async (req, user) => ({ - event: await getTeamEvent(req.params.uuid, user) -})); -loginCheckedGet('/getAllTeamEvents', async (_, user) => ({ events: await getAllTeamEvents(user) })); -loginCheckedPost('/updateTeamEvent', async (req, user) => ({ - event: await updateTeamEvent(req.body, user) -})); -loginCheckedPost('/deleteTeamEvent', async (req, user) => { - await deleteTeamEvent(req.body, user); - return {}; -}); -loginCheckedDelete('/clearAllTeamEvents', async (_, user) => { - await clearAllTeamEvents(user); - return {}; -}); -loginCheckedGet('/getAllTeamEventInfo', async () => ({ - allTeamEventInfo: await getAllTeamEventInfo() -})); -loginCheckedPost('/requestTeamEventCredit', async (req, user) => { - await requestTeamEventCredit(req.body.request, user); - return {}; -}); -loginCheckedGet('/getTeamEventAttendanceByUser', async (_, user) => ({ - teamEventAttendance: await getTeamEventAttendanceByUser(user) -})); -loginCheckedPost('/updateTeamEventAttendance', async (req, user) => ({ - teamEventAttendance: await updateTeamEventAttendance(req.body, user) -})); -loginCheckedPost('/deleteTeamEventAttendance', async (req, user) => { - await deleteTeamEventAttendance(req.body.uuid, user); - return {}; -}); - -// Team Events Proof Image -loginCheckedGet('/getEventProofImage/:name(*)', async (req, user) => ({ - url: await getEventProofImage(req.params.name, user) -})); -loginCheckedGet('/getEventProofImageSignedURL/:name(*)', async (req, user) => ({ - url: await setEventProofImage(req.params.name, user) -})); -loginCheckedPost('/deleteEventProofImage', async (req, user) => { - await deleteEventProofImage(req.body.name, user); - return {}; -}); - -// Candidate Decider -loginCheckedGet('/getAllCandidateDeciderInstances', async (_, user) => ({ - instances: await getAllCandidateDeciderInstances(user) -})); -loginCheckedGet('/getCandidateDeciderInstance/:uuid', async (req, user) => ({ - instance: await getCandidateDeciderInstance(req.params.uuid, user) -})); -loginCheckedPost('/createNewCandidateDeciderInstance', async (req, user) => ({ - instance: await createNewCandidateDeciderInstance(req.body, user) -})); -loginCheckedPost('/toggleCandidateDeciderInstance', async (req, user) => - toggleCandidateDeciderInstance(req.body.uuid, user).then(() => ({})) -); -loginCheckedPost('/deleteCandidateDeciderInstance', async (req, user) => - deleteCandidateDeciderInstance(req.body.uuid, user).then(() => ({})) -); -loginCheckedPost('/updateCandidateDeciderRating', (req, user) => - updateCandidateDeciderRating(user, req.body.uuid, req.body.id, req.body.rating).then(() => ({})) -); -loginCheckedPost('/updateCandidateDeciderComment', (req, user) => - updateCandidateDeciderComment(user, req.body.uuid, req.body.id, req.body.comment).then(() => ({})) -); -loginCheckedPost('/sendMail', async (req, user) => ({ - info: await sendMail(req.body.to, req.body.subject, req.body.text) -})); +router.use('/member', memberRouter); +router.use('/memberDiffs', memberDiffRouter); +router.use('/team', teamRouter); +router.use('/memberImage', memberImageRouter); +router.use('/shoutout', shoutoutRouter); +router.use('/', siteIntegrationRouter); +router.use('/', signInRouter); +router.use('/team-event', teamEventRouter); +router.use('/event-proof-image', eventProofImageRouter); +router.use('/candidate-decider', candidateDeciderRouter); +router.use('/dev-portfolio', devPortfolioRouter); // Dev Portfolios -loginCheckedGet('/getAllDevPortfolios', async (req, user) => ({ - portfolios: await getAllDevPortfolios(user) -})); -loginCheckedGet('/getAllDevPortfolioInfo', async (req, user) => ({ - portfolioInfo: await getAllDevPortfolioInfo() -})); -loginCheckedGet('/getDevPortfolioInfo/:uuid', async (req, user) => ({ - portfolioInfo: await getDevPortfolioInfo(req.params.uuid) -})); -loginCheckedGet('/getUsersDevPortfolioSubmissions/:uuid', async (req, user) => ({ - submissions: await getUsersDevPortfolioSubmissions(req.params.uuid, user) -})); -loginCheckedGet('/getDevPortfolio/:uuid', async (req, user) => ({ - portfolio: await getDevPortfolio(req.params.uuid, user) -})); -loginCheckedPost('/createNewDevPortfolio', async (req, user) => ({ - portfolio: await createNewDevPortfolio(req.body, user) -})); -loginCheckedPost('/deleteDevPortfolio', async (req, user) => - deleteDevPortfolio(req.body.uuid, user).then(() => ({})) -); -loginCheckedPost('/makeDevPortfolioSubmission', async (req, user) => { - await DPSubmissionRequestLogDao.logRequest(user.email, req.body.uuid, req.body.submission); - return { - submission: await makeDevPortfolioSubmission(req.body.uuid, req.body.submission) - }; -}); -loginCheckedPost('/regradeDevPortfolioSubmissions', async (req, user) => ({ - portfolio: await regradeSubmissions(req.body.uuid, user) -})); -loginCheckedPost('/updateDevPortfolioSubmissions', async (req, user) => ({ - portfolio: await updateSubmissions(req.body.uuid, req.body.updatedSubmissions, user) -})); app.use('/.netlify/functions/api', router); diff --git a/backend/src/dao/AuthRoleDao.ts b/backend/src/dao/AuthRoleDao.ts new file mode 100644 index 000000000..e1d84d3bd --- /dev/null +++ b/backend/src/dao/AuthRoleDao.ts @@ -0,0 +1,18 @@ +import BaseDao from './BaseDao'; +import { authRoleCollection } from '../firebase'; +import { AuthRoleDoc } from '../types/AuthTypes'; + +export default class AuthRoleDao extends BaseDao { + constructor() { + super( + authRoleCollection, + async (doc) => doc, + async (data) => data + ); + } + + async getAuthRole(user: IdolMember): Promise { + const authRoleData = await this.getDocument(user.email); + return authRoleData || undefined; + } +} diff --git a/backend/src/firebase.ts b/backend/src/firebase.ts index d86a0807a..9b37b5533 100644 --- a/backend/src/firebase.ts +++ b/backend/src/firebase.ts @@ -7,6 +7,7 @@ import { DevPortfolioSubmissionRequestLog, DBTeamEventAttendance } from './types/DataTypes'; +import { AuthRoleDoc } from './types/AuthTypes'; import { configureAccount } from './utils/firebase-utils'; require('dotenv').config(); @@ -133,3 +134,14 @@ export const devPortfolioSubmissionRequestLogCollection: admin.firestore.Collect }); export const adminCollection: admin.firestore.CollectionReference = db.collection('admins'); + +export const authRoleCollection: admin.firestore.CollectionReference = db + .collection('auth-role') + .withConverter({ + fromFirestore(snapshot): AuthRoleDoc { + return snapshot.data() as AuthRoleDoc; + }, + toFirestore(authRoleDoc: AuthRoleDoc) { + return authRoleDoc; + } + }); diff --git a/backend/src/routers/candidateDeciderRouter.ts b/backend/src/routers/candidateDeciderRouter.ts new file mode 100644 index 000000000..d5f4e3802 --- /dev/null +++ b/backend/src/routers/candidateDeciderRouter.ts @@ -0,0 +1,48 @@ +import { Router } from 'express'; +import { + loginCheckedDelete, + loginCheckedGet, + loginCheckedPost, + loginCheckedPut +} from '../utils/auth'; +import { + createNewCandidateDeciderInstance, + deleteCandidateDeciderInstance, + getAllCandidateDeciderInstances, + getCandidateDeciderInstance, + toggleCandidateDeciderInstance, + updateCandidateDeciderComment, + updateCandidateDeciderRating +} from '../API/candidateDeciderAPI'; + +const candidateDeciderRouter = Router(); + +// I think the flexibility of `userHasAccess` param for the auth middleware +// make it so that the auth doesn't have to be handled by the API router handler +// anymore. I just don't wanna do it. + +loginCheckedGet(candidateDeciderRouter, '/', async (_, user) => ({ + instances: await getAllCandidateDeciderInstances(user) +})); +loginCheckedGet(candidateDeciderRouter, '/:uuid', async (req, user) => ({ + instance: await getCandidateDeciderInstance(req.params.uuid, user) +})); +loginCheckedPost(candidateDeciderRouter, '/', async (req, user) => ({ + instance: await createNewCandidateDeciderInstance(req.body, user) +})); +loginCheckedPut(candidateDeciderRouter, '/:uuid', async (req, user) => + toggleCandidateDeciderInstance(req.params.uuid, user).then(() => ({})) +); +loginCheckedDelete(candidateDeciderRouter, '/:uuid', async (req, user) => + deleteCandidateDeciderInstance(req.params.uuid, user).then(() => ({})) +); +loginCheckedPut(candidateDeciderRouter, '/:uuid/rating', (req, user) => + updateCandidateDeciderRating(user, req.params.uuid, req.body.id, req.body.rating).then(() => ({})) +); +loginCheckedPost(candidateDeciderRouter, '/:uuid/comment', (req, user) => + updateCandidateDeciderComment(user, req.params.uuid, req.body.id, req.body.comment).then( + () => ({}) + ) +); + +export default candidateDeciderRouter; diff --git a/backend/src/routers/devPortfolioRouter.ts b/backend/src/routers/devPortfolioRouter.ts new file mode 100644 index 000000000..8b9edb966 --- /dev/null +++ b/backend/src/routers/devPortfolioRouter.ts @@ -0,0 +1,113 @@ +import { Router, Request } from 'express'; +import { + loginCheckedDelete, + loginCheckedGet, + loginCheckedPost, + loginCheckedPut +} from '../utils/auth'; +import { + getAllDevPortfolios, + getAllDevPortfolioInfo, + getDevPortfolio, + getDevPortfolioInfo, + createNewDevPortfolio, + deleteDevPortfolio, + makeDevPortfolioSubmission, + regradeSubmissions, + updateSubmissions, + getUsersDevPortfolioSubmissions +} from '../API/devPortfolioAPI'; +import DPSubmissionRequestLogDao from '../dao/DPSubmissionRequestLogDao'; + +const devPortfolioRouter = Router(); + +const userCanAccessResource = async (req: Request, user: IdolMember): Promise => { + if (req.params.email === user.email) return true; + if (req.query.meta_only) return true; + return false; +}; + +// /dev-portfolio +loginCheckedGet( + devPortfolioRouter, + '/', + async (req, user) => ({ + portfolios: !req.query.meta_only + ? await getAllDevPortfolios(user) + : await getAllDevPortfolioInfo() + }), + 'dev-portfolio', + 'write', + userCanAccessResource +); +loginCheckedGet( + devPortfolioRouter, + '/:uuid', + async (req, user) => ({ + portfolio: !req.query.meta_only + ? await getDevPortfolio(req.params.uuid, user) + : await getDevPortfolioInfo(req.params.uuid) + }), + 'dev-portfolio', + 'read', + userCanAccessResource +); + +loginCheckedPost( + devPortfolioRouter, + '/', + async (req, user) => ({ + portfolio: await createNewDevPortfolio(req.body, user) + }), + 'dev-portfolio', + 'write', + userCanAccessResource +); +loginCheckedDelete( + devPortfolioRouter, + '/:uuid', + async (req, user) => deleteDevPortfolio(req.params.uuid, user).then(() => ({})), + 'dev-portfolio', + 'write', + userCanAccessResource +); + +// devPortfolioSubmissionRouter: /dev-portfolio/:uuid/submission +loginCheckedPost(devPortfolioRouter, '/:uuid/submission', async (req, user) => { + await DPSubmissionRequestLogDao.logRequest(user.email, req.body.uuid, req.body.submission); + return { + submission: await makeDevPortfolioSubmission(req.body.uuid, req.body.submission) + }; +}); +loginCheckedPut( + devPortfolioRouter, + '/:uuid/submission/regrade', + async (req, user) => ({ + portfolio: await regradeSubmissions(req.body.uuid, user) + }), + 'dev-portfolio-submission', + 'write', + async () => false +); +loginCheckedPut( + devPortfolioRouter, + '/:uuid/submission', + async (req, user) => ({ + portfolio: await updateSubmissions(req.params.uuid, req.body.updatedSubmissions, user) + }), + 'dev-portfolio-submission', + 'write', + async () => false +); +loginCheckedGet( + devPortfolioRouter, + '/:uuid/submission/:email', + async (req, user) => ({ + submissions: await getUsersDevPortfolioSubmissions(req.params.uuid, user) + }), + 'dev-portfolio-submission', + 'read', + userCanAccessResource +); + +export default devPortfolioRouter; diff --git a/backend/src/routers/imageRouter.ts b/backend/src/routers/imageRouter.ts new file mode 100644 index 000000000..9022b27e5 --- /dev/null +++ b/backend/src/routers/imageRouter.ts @@ -0,0 +1,37 @@ +import { Router, Request } from 'express'; +import { getMemberImage, setMemberImage, allMemberImages } from '../API/imageAPI'; +import { loginCheckedGet } from '../utils/auth'; + +const memberImageRouter = Router(); + +const canAccessResource = async (req: Request, user: IdolMember): Promise => + req.params.email === user.email; + +loginCheckedGet( + memberImageRouter, + '/:email', + async (_, user) => ({ + url: await getMemberImage(user) + }), + 'profile-image', + 'read', + canAccessResource +); + +loginCheckedGet( + memberImageRouter, + '/:email/signed-url', + async (_, user) => ({ + url: await setMemberImage(user) + }), + 'profile-image', + 'write', + canAccessResource +); + +memberImageRouter.get('/', async (_, res) => { + const images = await allMemberImages(); + res.status(200).json({ images }); +}); + +export default memberImageRouter; diff --git a/backend/src/routers/memberRouter.ts b/backend/src/routers/memberRouter.ts new file mode 100644 index 000000000..82be4ac83 --- /dev/null +++ b/backend/src/routers/memberRouter.ts @@ -0,0 +1,92 @@ +import { Router, Request } from 'express'; +import { + allApprovedMembers, + allMembers, + setMember, + deleteMember, + updateMember, + getUserInformationDifference, + reviewUserInformationChange +} from '../API/memberAPI'; +import MembersDao from '../dao/MembersDao'; +import { + loginCheckedPost, + loginCheckedDelete, + loginCheckedPut, + loginCheckedGet +} from '../utils/auth'; + +const canAccessResource = async (req: Request, user: IdolMember): Promise => + req.params.email === user.email; + +export const memberRouter = Router(); +export const memberDiffRouter = Router(); + +memberRouter.get('/', async (req, res) => { + const type = req.query.type as string | undefined; + let members; + switch (type) { + case 'all-semesters': + members = await MembersDao.getMembersFromAllSemesters(); + break; + case 'approved': + members = await allApprovedMembers(); + break; + default: + members = await allMembers(); + } + res.status(200).json({ members }); +}); + +loginCheckedPost( + memberRouter, + '/:email', + async (req, user) => ({ + member: await setMember(req.body, user) + }), + 'member', + 'write', + canAccessResource +); +loginCheckedDelete( + memberRouter, + '/:email', + async (req, user) => { + await deleteMember(req.params.email, user); + return {}; + }, + 'member', + 'write', + async () => false +); +loginCheckedPut( + memberRouter, + '/:email', + async (req, user) => ({ + member: await updateMember(req, req.body, user) + }), + 'member', + 'write', + canAccessResource +); + +loginCheckedGet( + memberDiffRouter, + '/', + async (_, user) => ({ + diffs: await getUserInformationDifference(user) + }), + 'member-diff', + 'read', + async () => false +); +loginCheckedPut( + memberDiffRouter, + '/', + async (req, user) => ({ + member: await reviewUserInformationChange(req.body.approved, req.body.rejected, user) + }), + 'member-diff', + 'write', + async () => false +); diff --git a/backend/src/routers/shoutoutRouter.ts b/backend/src/routers/shoutoutRouter.ts new file mode 100644 index 000000000..e0cfdda34 --- /dev/null +++ b/backend/src/routers/shoutoutRouter.ts @@ -0,0 +1,70 @@ +import { Router, Request } from 'express'; +import { + getShoutouts, + getAllShoutouts, + giveShoutout, + hideShoutout, + deleteShoutout +} from '../API/shoutoutAPI'; +import { + loginCheckedGet, + loginCheckedPost, + loginCheckedPut, + loginCheckedDelete +} from '../utils/auth'; + +const shoutoutRouter = Router(); + +const canAccessResource = async (req: Request, user: IdolMember): Promise => + req.params.email === user.email; + +loginCheckedGet( + shoutoutRouter, + '/:email/:type', + async (req, user) => ({ + shoutouts: await getShoutouts(req.params.email, req.params.type as 'given' | 'received', user) + }), + 'shoutout', + 'read', + canAccessResource +); + +loginCheckedGet( + shoutoutRouter, + '/', + async () => ({ + shoutouts: await getAllShoutouts() + }), + 'shoutout', + 'read', + async () => false +); +// No RBAC? +loginCheckedPost(shoutoutRouter, '/', async (req, user) => ({ + shoutout: await giveShoutout(req.body, user) +})); + +loginCheckedPut( + shoutoutRouter, + '/', + async (req, user) => { + await hideShoutout(req.body.uuid, req.body.hide, user); + return {}; + }, + 'shoutout', + 'write', + async () => false +); + +loginCheckedDelete( + shoutoutRouter, + '/:uuid', + async (req, user) => { + await deleteShoutout(req.params.uuid, user); + return {}; + }, + 'shoutout', + 'write', + async () => false +); +export default shoutoutRouter; diff --git a/backend/src/routers/signInRouter.ts b/backend/src/routers/signInRouter.ts new file mode 100644 index 000000000..d91fc186a --- /dev/null +++ b/backend/src/routers/signInRouter.ts @@ -0,0 +1,35 @@ +import { Router } from 'express'; +import { + signInFormExists, + signInFormExpired, + createSignInForm, + deleteSignInForm, + signIn, + allSignInForms, + getSignInPrompt +} from '../API/signInFormAPI'; +import { loginCheckedPost, loginCheckedGet } from '../utils/auth'; + +const signInRouter = Router(); +loginCheckedPost(signInRouter, '/signInExists', async (req, _) => ({ + exists: await signInFormExists(req.body.id) +})); +loginCheckedPost(signInRouter, '/signInExpired', async (req, _) => ({ + expired: await signInFormExpired(req.body.id) +})); +loginCheckedPost(signInRouter, '/signInCreate', async (req, user) => + createSignInForm(req.body.id, req.body.expireAt, req.body.prompt, user) +); +loginCheckedPost(signInRouter, '/signInDelete', async (req, user) => { + await deleteSignInForm(req.body.id, user); + return {}; +}); +loginCheckedPost(signInRouter, '/signIn', async (req, user) => + signIn(req.body.id, req.body.response, user) +); +loginCheckedPost(signInRouter, '/signInAll', async (_, user) => allSignInForms(user)); +loginCheckedGet(signInRouter, '/signInPrompt/:id', async (req, _) => ({ + prompt: await getSignInPrompt(req.params.id) +})); + +export default signInRouter; diff --git a/backend/src/routers/siteIntegrationRouter.ts b/backend/src/routers/siteIntegrationRouter.ts new file mode 100644 index 000000000..7db2d3b57 --- /dev/null +++ b/backend/src/routers/siteIntegrationRouter.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import { + requestIDOLPullDispatch, + getIDOLChangesPR, + acceptIDOLChanges, + rejectIDOLChanges +} from '../API/siteIntegrationAPI'; +import { loginCheckedPost, loginCheckedGet } from '../utils/auth'; + +const siteIntegrationRouter = Router(); + +loginCheckedPost(siteIntegrationRouter, '/pullIDOLChanges', (_, user) => + requestIDOLPullDispatch(user) +); + +loginCheckedGet(siteIntegrationRouter, '/getIDOLChangesPR', (_, user) => getIDOLChangesPR(user)); + +loginCheckedPost(siteIntegrationRouter, '/acceptIDOLChanges', (_, user) => acceptIDOLChanges(user)); + +loginCheckedPost(siteIntegrationRouter, '/rejectIDOLChanges', (_, user) => rejectIDOLChanges(user)); + +export default siteIntegrationRouter; diff --git a/backend/src/routers/teamEventRouter.ts b/backend/src/routers/teamEventRouter.ts new file mode 100644 index 000000000..b6d70e1e7 --- /dev/null +++ b/backend/src/routers/teamEventRouter.ts @@ -0,0 +1,132 @@ +import { Router, Request } from 'express'; +import { + createTeamEvent, + getTeamEvent, + getAllTeamEvents, + getAllTeamEventInfo, + updateTeamEvent, + deleteTeamEvent, + clearAllTeamEvents, + requestTeamEventCredit, + getTeamEventAttendanceByUser, + updateTeamEventAttendance, + deleteTeamEventAttendance +} from '../API/teamEventsAPI'; +import { + loginCheckedPost, + loginCheckedGet, + loginCheckedPut, + loginCheckedDelete +} from '../utils/auth'; + +const teamEventRouter = Router(); + +const canAccessResource = async (req: Request, user: IdolMember): Promise => + req.query.meta_only === 'true' || req.params.email === user.email; + +// /team-event +loginCheckedPost( + teamEventRouter, + '/', + async (req, user) => { + await createTeamEvent(req.body, user); + return {}; + }, + 'team-event', + 'write', + async () => false +); +loginCheckedGet( + teamEventRouter, + '/:uuid', + async (req, user) => ({ + event: await getTeamEvent(req.params.uuid, user) + }), + 'team-event', + 'read', + canAccessResource +); +loginCheckedGet( + teamEventRouter, + '/', + async (req, user) => ({ + events: !req.query.meta_only ? await getAllTeamEvents(user) : await getAllTeamEventInfo() + }), + 'team-event', + 'read', + canAccessResource +); +loginCheckedPut( + teamEventRouter, + '/', + async (req, user) => ({ + event: await updateTeamEvent(req.body, user) + }), + 'team-event', + 'write', + async () => false +); +loginCheckedDelete( + teamEventRouter, + '/:uuid', + async (req, user) => { + await deleteTeamEvent(req.body, user); + return {}; + }, + 'team-event', + 'write', + async () => false +); +loginCheckedDelete( + teamEventRouter, + '/', + async (_, user) => { + await clearAllTeamEvents(user); + return {}; + }, + 'team-event', + 'write', + async () => false +); + +// /team-event/attendance +loginCheckedPost( + teamEventRouter, + '/attendance', + async (req, user) => { + await requestTeamEventCredit(req.body.request, user); + return {}; + }, + 'team-event-attendance' +); +loginCheckedGet( + teamEventRouter, + '/attendance/:email', + async (_, user) => ({ + teamEventAttendance: await getTeamEventAttendanceByUser(user) + }), + 'team-event-attendance', + 'read', + canAccessResource +); +loginCheckedPut( + teamEventRouter, + '/attendance', + async (req, user) => ({ + teamEventAttendance: await updateTeamEventAttendance(req.body, user) + }), + 'team-event-attendance' +); +loginCheckedDelete( + teamEventRouter, + '/attendance/:uuid', + async (req, user) => { + await deleteTeamEventAttendance(req.params.uuid, user); + return {}; + }, + 'team-event-attendance', + 'write', + async () => false +); + +export default teamEventRouter; diff --git a/backend/src/routers/teamEventsImageRouter.ts b/backend/src/routers/teamEventsImageRouter.ts new file mode 100644 index 000000000..f6c8e27ec --- /dev/null +++ b/backend/src/routers/teamEventsImageRouter.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import { loginCheckedGet, loginCheckedDelete } from '../utils/auth'; +import { + getEventProofImage, + setEventProofImage, + deleteEventProofImage +} from '../API/teamEventsImageAPI'; + +const eventProofImageRouter = Router(); + +loginCheckedGet(eventProofImageRouter, '/:name(*)', async (req, user) => ({ + url: await getEventProofImage(req.params.name, user) +})); +loginCheckedGet(eventProofImageRouter, '/:name(*)/signed-url', async (req, user) => ({ + url: await setEventProofImage(req.params.name, user) +})); +loginCheckedDelete(eventProofImageRouter, '/:name', async (req, user) => { + await deleteEventProofImage(req.params.name, user); + return {}; +}); + +export default eventProofImageRouter; diff --git a/backend/src/routers/teamRouter.ts b/backend/src/routers/teamRouter.ts new file mode 100644 index 000000000..52f89f80d --- /dev/null +++ b/backend/src/routers/teamRouter.ts @@ -0,0 +1,39 @@ +import { Router } from 'express'; +import { allTeams, setTeam, deleteTeam } from '../API/teamAPI'; +import { loginCheckedGet, loginCheckedPut, loginCheckedPost } from '../utils/auth'; + +const teamRouter = Router(); + +loginCheckedGet( + teamRouter, + '/', + async () => ({ teams: await allTeams() }), + 'team', + 'read', + async () => false +); + +loginCheckedPut( + teamRouter, + '/', + async (req, user) => ({ + team: await setTeam(req.body, user) + }), + 'team', + 'write', + async () => false +); + +// TODO: should eventually make this a delete request +loginCheckedPost( + teamRouter, + '/', + async (req, user) => ({ + team: await deleteTeam(req.body, user) + }), + 'team', + 'write', + async () => false +); + +export default teamRouter; diff --git a/backend/src/types/AuthTypes.d.ts b/backend/src/types/AuthTypes.d.ts new file mode 100644 index 000000000..63d88aa9b --- /dev/null +++ b/backend/src/types/AuthTypes.d.ts @@ -0,0 +1,33 @@ +type AuthLeadTypes = + | 'dev_lead' + | 'ops_lead' + | 'lead' + | 'design_lead' + | 'business_lead' + | 'ci_lead' + | 'pm_lead'; + +export type AuthRole = + | undefined + | 'admin' + | 'dev' + | 'tpm' + | 'pm' + | 'designer' + | 'business' + | 'lead' + | AuthLeadTypes; + +export interface AuthRoleDoc { + role: AuthRole; + leadType?: AuthLeadTypes; +} + +export interface RBACConfig { + resources: { + [resourceName: string]: { + read: AuthRole[]; + write: AuthRole[]; + }; + }; +} diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts new file mode 100644 index 000000000..0bdb4261d --- /dev/null +++ b/backend/src/utils/auth.ts @@ -0,0 +1,142 @@ +import { RequestHandler, Request, Response, Router, NextFunction } from 'express'; +import admin from 'firebase-admin'; +import { HandlerError } from './errors'; +import PermissionsManager from './permissionsManager'; +import { app as adminApp, env } from '../firebase'; +import MembersDao from '../dao/MembersDao'; +import rbacConfig from '../../rbac.json'; +import { AuthRoleDoc, RBACConfig } from '../types/AuthTypes'; +import AuthRoleDao from '../dao/AuthRoleDao'; + +const getUserEmailFromRequest = async (request: Request): Promise => { + const idToken = request.headers['auth-token']; + if (typeof idToken !== 'string') return undefined; + const decodedToken = await admin.auth(adminApp).verifyIdToken(idToken); + return decodedToken.email; +}; + +const getUserRole = async (user: IdolMember): Promise => { + const authRoleDao = new AuthRoleDao(); + return authRoleDao.getAuthRole(user); +}; + +const loginCheckedHandler = + (handler: (req: Request, user: IdolMember) => Promise>): RequestHandler => + async (req: Request, res: Response): Promise => { + const { user } = res.locals; + try { + res.status(200).send(await handler(req, user)); + } catch (error) { + if (error instanceof HandlerError) { + res.status(error.errorCode).send({ error: error.reason }); + return; + } + res.status(500).send({ error: `Failed to handle the request due to ${error}.` }); + } + }; + +const hasRbacPerm = async ( + user: IdolMember, + rbacConfig: RBACConfig, + resource?: string, + action?: string +): Promise => { + if (resource && action) { + const roleData = await getUserRole(user); + const userRole = roleData?.role; + const resourceRBACConfig = rbacConfig.resources[resource]; + + if (resourceRBACConfig) { + return resourceRBACConfig[action].includes(userRole); + } + } + return true; +}; + +const getAuthMiddleware = + ( + resource?: string, + action?: string, + userHasAccess: (req: Request, user: IdolMember) => Promise = async () => true + ): RequestHandler => + async (req: Request, res: Response, next: NextFunction) => { + // authentication + const userEmail = await getUserEmailFromRequest(req); + if (userEmail == null) { + res.status(440).json({ error: 'Not logged in!' }); + return; + } + const user = await MembersDao.getCurrentOrPastMemberByEmail(userEmail); + if (!user) { + res.status(401).send({ error: `No user with email: ${userEmail}` }); + return; + } + if (env === 'staging' && !(await PermissionsManager.isAdmin(user))) { + res.status(401).json({ error: 'Only admins users have permismsions to the staging API!' }); + } + // RBAC + const userHasRbacPerm = await hasRbacPerm(user, rbacConfig as RBACConfig, resource, action); + if (!(userHasRbacPerm || (await userHasAccess(req, user)))) { + res.status(401).send({ + error: `User with email ${user.email} does not have read and/or write access to the requested resource.` + }); + } + res.locals.user = user; + next(); + }; + +export const loginCheckedGet = ( + router: Router, + path: string, + handler: (req: Request, user: IdolMember) => Promise>, + resource?: string, + action?: string, + userHasAccess?: (req: Request, user: IdolMember) => Promise +): RequestHandler => + router.get( + path, + getAuthMiddleware(resource, action, userHasAccess), + loginCheckedHandler(handler) + ); + +export const loginCheckedPost = ( + router: Router, + path: string, + handler: (req: Request, user: IdolMember) => Promise>, + resource?: string, + action?: string, + userHasAccess?: (req: Request, user: IdolMember) => Promise +): RequestHandler => + router.post( + path, + getAuthMiddleware(resource, action, userHasAccess), + loginCheckedHandler(handler) + ); + +export const loginCheckedDelete = ( + router: Router, + path: string, + handler: (req: Request, user: IdolMember) => Promise>, + resource?: string, + action?: string, + userHasAccess?: (req: Request, user: IdolMember) => Promise +): RequestHandler => + router.delete( + path, + getAuthMiddleware(resource, action, userHasAccess), + loginCheckedHandler(handler) + ); + +export const loginCheckedPut = ( + router: Router, + path: string, + handler: (req: Request, user: IdolMember) => Promise>, + resource?: string, + action?: string, + userHasAccess?: (req: Request, user: IdolMember) => Promise +): RequestHandler => + router.put( + path, + getAuthMiddleware(resource, action, userHasAccess), + loginCheckedHandler(handler) + ); diff --git a/frontend/src/API/CandidateDeciderAPI.ts b/frontend/src/API/CandidateDeciderAPI.ts index 0083aff55..02fb3851f 100644 --- a/frontend/src/API/CandidateDeciderAPI.ts +++ b/frontend/src/API/CandidateDeciderAPI.ts @@ -3,35 +3,35 @@ import { backendURL } from '../environment'; export default class CandidateDeciderAPI { static async getAllInstances(): Promise { - const response = APIWrapper.get(`${backendURL}/getAllCandidateDeciderInstances`); + const response = APIWrapper.get(`${backendURL}/candidate-decider`); return response.then((val) => val.data.instances); } static async getInstance(uuid: string): Promise { - const response = APIWrapper.get(`${backendURL}/getCandidateDeciderInstance/${uuid}`); + const response = APIWrapper.get(`${backendURL}/candidate-decider/${uuid}`); return response.then((val) => val.data.instance); } static async createNewInstance( instance: CandidateDeciderInstance ): Promise { - const response = APIWrapper.post(`${backendURL}/createNewCandidateDeciderInstance`, instance); + const response = APIWrapper.post(`${backendURL}/candidate-decider`, instance); return response.then((val) => val.data.instance); } static async toggleInstance(uuid: string): Promise { - APIWrapper.post(`${backendURL}/toggleCandidateDeciderInstance`, { uuid }); + APIWrapper.put(`${backendURL}/candidate-decider/${uuid}`, {}); } static async deleteInstance(uuid: string): Promise { - APIWrapper.post(`${backendURL}/deleteCandidateDeciderInstance`, { uuid }); + APIWrapper.delete(`${backendURL}/candidate-decider/${uuid}`); } static async updateRating(uuid: string, id: number, rating: number): Promise { - APIWrapper.post(`${backendURL}/updateCandidateDeciderRating`, { uuid, id, rating }); + APIWrapper.post(`${backendURL}/candidate-decider/${uuid}/rating`, { uuid, id, rating }); } static async updateComment(uuid: string, id: number, comment: string): Promise { - APIWrapper.post(`${backendURL}/updateCandidateDeciderComment`, { uuid, id, comment }); + APIWrapper.post(`${backendURL}/candidate-decider/${uuid}/comment`, { uuid, id, comment }); } } diff --git a/frontend/src/API/DevPortfolioAPI.ts b/frontend/src/API/DevPortfolioAPI.ts index d3be97e4d..4e1d6c4df 100644 --- a/frontend/src/API/DevPortfolioAPI.ts +++ b/frontend/src/API/DevPortfolioAPI.ts @@ -7,17 +7,17 @@ type DevPortfolioSubmissionResponseObj = { export default class DevPortfolioAPI { static async getAllDevPortfolios(): Promise { - const response = APIWrapper.get(`${backendURL}/getAllDevPortfolios`); + const response = APIWrapper.get(`${backendURL}/dev-portfolio`); return response.then((val) => val.data.portfolios); } static async getAllDevPortfolioInfo(): Promise { - const response = APIWrapper.get(`${backendURL}/getAllDevPortfolioInfo`); - return response.then((val) => val.data.portfolioInfo); + const response = APIWrapper.get(`${backendURL}/dev-portfolio?meta_only=true`); + return response.then((val) => val.data.portfolios); } public static async createDevPortfolio(devPortfolio: DevPortfolio): Promise { - return APIWrapper.post(`${backendURL}/createNewDevPortfolio`, devPortfolio).then( + return APIWrapper.post(`${backendURL}/dev-portfolio`, devPortfolio).then( (res) => res.data.portfolio ); } @@ -26,38 +26,37 @@ export default class DevPortfolioAPI { uuid: string, submission: DevPortfolioSubmission ): Promise { - return APIWrapper.post(`${backendURL}/makeDevPortfolioSubmission`, { + return APIWrapper.post(`${backendURL}/dev-portfolio/${uuid}/submission`, { uuid, submission }).then((res) => res.data); } public static async deleteDevPortfolio(uuid: string): Promise { - APIWrapper.post(`${backendURL}/deleteDevPortfolio`, { uuid }); + APIWrapper.delete(`${backendURL}/dev-portfolio/${uuid}`); } public static async getDevPortfolio(uuid: string): Promise { - return APIWrapper.get(`${backendURL}/getDevPortfolio/${uuid}`).then( - (res) => res.data.portfolio - ); + return APIWrapper.get(`${backendURL}/dev-portfolio/${uuid}`).then((res) => res.data.portfolio); } public static async getDevPortfolioInfo(uuid: string): Promise { - return APIWrapper.get(`${backendURL}/getDevPortfolioInfo/${uuid}`).then( - (res) => res.data.portfolioInfo + return APIWrapper.get(`${backendURL}/dev-portfolio/${uuid}?meta_only=true`).then( + (res) => res.data.portfolio ); } public static async getUsersDevPortfolioSubmissions( - uuid: string + uuid: string, + email: string ): Promise { - return APIWrapper.get(`${backendURL}/getUsersDevPortfolioSubmissions/${uuid}`).then( + return APIWrapper.get(`${backendURL}/dev-portfolio/${uuid}/submission/${email}`).then( (res) => res.data.submissions ); } public static async regradeSubmissions(uuid: string): Promise { - return APIWrapper.post(`${backendURL}/regradeDevPortfolioSubmissions`, { uuid }).then( + return APIWrapper.put(`${backendURL}/dev-portfolio/${uuid}/submission/regrade`, { uuid }).then( (res) => res.data.portfolio ); } @@ -66,7 +65,7 @@ export default class DevPortfolioAPI { uuid: string, updatedSubmissions: DevPortfolioSubmission[] ): Promise { - return APIWrapper.post(`${backendURL}/updateDevPortfolioSubmissions`, { + return APIWrapper.put(`${backendURL}/dev-portfolio/${uuid}/submission`, { uuid, updatedSubmissions }).then((res) => res.data.portfolio); diff --git a/frontend/src/API/ImagesAPI.ts b/frontend/src/API/ImagesAPI.ts index dd0d06bb0..6e8f48039 100644 --- a/frontend/src/API/ImagesAPI.ts +++ b/frontend/src/API/ImagesAPI.ts @@ -4,8 +4,10 @@ import HeadshotPlaceholder from '../static/images/headshot-placeholder.png'; export default class ImagesAPI { // member images - public static getMemberImage(): Promise { - const responseProm = APIWrapper.get(`${backendURL}/getMemberImage`).then((res) => res.data); + public static getMemberImage(email: string): Promise { + const responseProm = APIWrapper.get(`${backendURL}/memberImage/${email}`).then( + (res) => res.data + ); return responseProm.then((val) => { if (val.error) { @@ -15,13 +17,15 @@ export default class ImagesAPI { }); } - private static getSignedURL(): Promise { - const responseProm = APIWrapper.get(`${backendURL}/getImageSignedURL`).then((res) => res.data); + private static getSignedURL(email: string): Promise { + const responseProm = APIWrapper.get(`${backendURL}/memberImage/${email}/signed-url`).then( + (res) => res.data + ); return responseProm.then((val) => val.url); } - public static uploadMemberImage(body: Blob): Promise { - return this.getSignedURL().then((url) => { + public static uploadMemberImage(body: Blob, email: string): Promise { + return this.getSignedURL(email).then((url) => { const headers = { 'content-type': 'image/jpeg' }; APIWrapper.put(url, body, headers).then((res) => res.data); }); @@ -29,7 +33,7 @@ export default class ImagesAPI { // Event proof images public static getEventProofImage(name: string): Promise { - const responseProm = APIWrapper.get(`${backendURL}/getEventProofImage/${name}`).then( + const responseProm = APIWrapper.get(`${backendURL}/event-proof-image/${name}`).then( (res) => res.data ); return responseProm.then((val) => { @@ -41,7 +45,7 @@ export default class ImagesAPI { } private static getEventProofImageSignedURL(name: string): Promise { - const responseProm = APIWrapper.get(`${backendURL}/getEventProofImageSignedURL/${name}`).then( + const responseProm = APIWrapper.get(`${backendURL}/event-proof-image/${name}/signed-url`).then( (res) => res.data ); return responseProm.then((val) => val.url); @@ -55,6 +59,6 @@ export default class ImagesAPI { } public static async deleteEventProofImage(name: string): Promise { - await APIWrapper.post(`${backendURL}/deleteEventProofImage`, { name }); + await APIWrapper.post(`${backendURL}/event-proof-image/${name}`, { name }); } } diff --git a/frontend/src/API/MembersAPI.ts b/frontend/src/API/MembersAPI.ts index 8e1722e47..9b448c1b4 100644 --- a/frontend/src/API/MembersAPI.ts +++ b/frontend/src/API/MembersAPI.ts @@ -10,19 +10,19 @@ export type Member = IdolMember; export class MembersAPI { public static async getMembersFromAllSemesters(): Promise> { - return APIWrapper.get(`${backendURL}/membersFromAllSemesters`).then((res) => res.data); + return APIWrapper.get(`${backendURL}/member?type=all-semesters`).then((res) => res.data); } public static setMember(member: Member): Promise { - return APIWrapper.post(`${backendURL}/setMember`, member).then((res) => res.data); + return APIWrapper.post(`${backendURL}/member/${member.email}`, member).then((res) => res.data); } public static deleteMember(memberEmail: string): Promise<{ status: number; error?: string }> { - return APIWrapper.delete(`${backendURL}/deleteMember/${memberEmail}`).then((res) => res.data); + return APIWrapper.delete(`${backendURL}/member/${memberEmail}`).then((res) => res.data); } public static updateMember(member: Member): Promise { - return APIWrapper.post(`${backendURL}/updateMember`, member).then((res) => res.data); + return APIWrapper.put(`${backendURL}/member/${member.email}`, member).then((res) => res.data); } public static hasIDOLAccess(email: string): Promise { diff --git a/frontend/src/API/ShoutoutsAPI.ts b/frontend/src/API/ShoutoutsAPI.ts index 401272df1..27c770c90 100644 --- a/frontend/src/API/ShoutoutsAPI.ts +++ b/frontend/src/API/ShoutoutsAPI.ts @@ -9,7 +9,7 @@ type ShoutoutResponseObj = { export default class ShoutoutsAPI { public static getAllShoutouts(): Promise { - const responseProm = APIWrapper.get(`${backendURL}/allShoutouts`).then((res) => res.data); + const responseProm = APIWrapper.get(`${backendURL}/shoutout`).then((res) => res.data); return responseProm.then((val) => { if (val.error) { Emitters.generalError.emit({ @@ -24,7 +24,7 @@ export default class ShoutoutsAPI { } public static getShoutouts(email: string, type: 'given' | 'received'): Promise { - const responseProm = APIWrapper.get(`${backendURL}/getShoutouts/${email}/${type}`).then( + const responseProm = APIWrapper.get(`${backendURL}/shoutout/${email}/${type}`).then( (res) => res.data ); return responseProm.then((val) => { @@ -41,14 +41,14 @@ export default class ShoutoutsAPI { } public static giveShoutout(shoutout: Shoutout): Promise { - return APIWrapper.post(`${backendURL}/giveShoutout`, shoutout).then((res) => res.data); + return APIWrapper.post(`${backendURL}/shoutout`, shoutout).then((res) => res.data); } public static hideShoutout(uuid: string, hide: boolean): Promise { - return APIWrapper.post(`${backendURL}/hideShoutout`, { uuid, hide }).then((res) => res.data); + return APIWrapper.put(`${backendURL}/shoutout`, { uuid, hide }).then((res) => res.data); } public static async deleteShoutout(uuid: string): Promise { - await APIWrapper.post(`${backendURL}/deleteShoutout`, { uuid }); + await APIWrapper.delete(`${backendURL}/shoutout/${uuid}`); } } diff --git a/frontend/src/API/TeamEventsAPI.ts b/frontend/src/API/TeamEventsAPI.ts index 01f552a24..cf4f5edc1 100644 --- a/frontend/src/API/TeamEventsAPI.ts +++ b/frontend/src/API/TeamEventsAPI.ts @@ -18,7 +18,7 @@ export type MemberTECRequests = { export class TeamEventsAPI { public static getAllTeamEvents(): Promise { - const eventsProm = APIWrapper.get(`${backendURL}/getAllTeamEvents`).then((res) => res.data); + const eventsProm = APIWrapper.get(`${backendURL}/team-event`).then((res) => res.data); return eventsProm.then((val) => { if (val.error) { Emitters.generalError.emit({ @@ -33,7 +33,7 @@ export class TeamEventsAPI { } public static getAllTeamEventInfo(): Promise { - const res = APIWrapper.get(`${backendURL}/getAllTeamEventInfo`).then((res) => res.data); + const res = APIWrapper.get(`${backendURL}/team-event?meta_only=true`).then((res) => res.data); return res.then((val) => { if (val.error) { Emitters.generalError.emit({ @@ -48,7 +48,7 @@ export class TeamEventsAPI { } public static getTeamEventForm(uuid: string): Promise { - const eventProm = APIWrapper.get(`${backendURL}/getTeamEvent/${uuid}`).then((res) => res.data); + const eventProm = APIWrapper.get(`${backendURL}/team-event/${uuid}`).then((res) => res.data); return eventProm.then((val) => { const event = val.event as Event; return event; @@ -56,41 +56,41 @@ export class TeamEventsAPI { } public static createTeamEventForm(teamEventInfo: TeamEventInfo): Promise { - return APIWrapper.post(`${backendURL}/createTeamEvent`, teamEventInfo).then((res) => res.data); + return APIWrapper.post(`${backendURL}/team-event`, teamEventInfo).then((res) => res.data); } public static async deleteTeamEventForm(teamEvent: Event): Promise { - await APIWrapper.post(`${backendURL}/deleteTeamEvent`, teamEvent); + await APIWrapper.delete(`${backendURL}/team-event/${teamEvent.uuid}`); } public static updateTeamEventForm(teamEventInfo: TeamEventInfo): Promise { - return APIWrapper.post(`${backendURL}/updateTeamEvent`, teamEventInfo).then( + return APIWrapper.put(`${backendURL}/team-event`, teamEventInfo).then( (rest) => rest.data.event ); } public static async clearAllTeamEvents(): Promise { - await APIWrapper.delete(`${backendURL}/clearAllTeamEvents`); + await APIWrapper.delete(`${backendURL}/team-event`); } public static async requestTeamEventCredit(request: TeamEventAttendance): Promise { - APIWrapper.post(`${backendURL}/requestTeamEventCredit`, { request }); + APIWrapper.post(`${backendURL}/team-event/attendance`, { request }); } public static async deleteTeamEventAttendance(uuid: string): Promise { - await APIWrapper.post(`${backendURL}/deleteTeamEventAttendance`, { uuid }); + await APIWrapper.post(`${backendURL}/team-event/attendance/${uuid}`, { uuid }); } public static async updateTeamEventAttendance( teamEventAttendance: TeamEventAttendance ): Promise { - return APIWrapper.post(`${backendURL}/updateTeamEventAttendance`, teamEventAttendance).then( + return APIWrapper.put(`${backendURL}/team-event/attendance`, teamEventAttendance).then( (res) => res.data ); } - public static async getTeamEventAttendanceByUser(): Promise { - const res = APIWrapper.get(`${backendURL}/getTeamEventAttendanceByUser`).then( + public static async getTeamEventAttendanceByUser(email: string): Promise { + const res = APIWrapper.get(`${backendURL}/team-event/attendance/${email}`).then( (res) => res.data ); return res.then((val) => { diff --git a/frontend/src/API/TeamsAPI.ts b/frontend/src/API/TeamsAPI.ts index 702da197f..532b86afc 100644 --- a/frontend/src/API/TeamsAPI.ts +++ b/frontend/src/API/TeamsAPI.ts @@ -17,10 +17,10 @@ export type Team = { export class TeamsAPI { public static setTeam(team: Team): Promise { - return APIWrapper.post(`${backendURL}/setTeam`, team).then((res) => res.data); + return APIWrapper.put(`${backendURL}/team`, team).then((res) => res.data); } public static deleteTeam(team: Team): Promise { - return APIWrapper.post(`${backendURL}/deleteTeam`, team).then((res) => res.data); + return APIWrapper.post(`${backendURL}/team`, team).then((res) => res.data); } } diff --git a/frontend/src/components/Admin/DevPortfolio/DevPortfolioDetails.tsx b/frontend/src/components/Admin/DevPortfolio/DevPortfolioDetails.tsx index 02cc83ce1..12a8d901c 100644 --- a/frontend/src/components/Admin/DevPortfolio/DevPortfolioDetails.tsx +++ b/frontend/src/components/Admin/DevPortfolio/DevPortfolioDetails.tsx @@ -5,6 +5,7 @@ import DevPortfolioTextModal from '../../Modals/DevPortfolioTextModal'; import DevPortfolioAPI from '../../../API/DevPortfolioAPI'; import { Emitters } from '../../../utils'; import styles from './DevPortfolioDetails.module.css'; +import { useSelf } from '../../Common/FirestoreDataProvider'; type Props = { uuid: string; @@ -18,18 +19,23 @@ const DevPortfolioDetails: React.FC = ({ uuid, isAdminView }) => { const [portfolio, setPortfolio] = useState(null); const [isRegrading, setIsRegrading] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const userInfo = useSelf()!; + useEffect(() => { if (isAdminView) { DevPortfolioAPI.getDevPortfolio(uuid).then((portfolio) => setPortfolio(portfolio)); } else { DevPortfolioAPI.getDevPortfolioInfo(uuid).then((portfolioInfo) => { const portfolio = portfolioInfo as DevPortfolio; - DevPortfolioAPI.getUsersDevPortfolioSubmissions(uuid).then((portfolioSubmissions) => { - setPortfolio({ ...portfolio, submissions: portfolioSubmissions }); - }); + DevPortfolioAPI.getUsersDevPortfolioSubmissions(uuid, userInfo.email).then( + (portfolioSubmissions) => { + setPortfolio({ ...portfolio, submissions: portfolioSubmissions }); + } + ); }); } - }, [uuid, isAdminView]); + }, [uuid, isAdminView, userInfo.email]); const handleExportToCsv = () => { if (portfolio?.submissions === undefined || portfolio?.submissions.length <= 0) { diff --git a/frontend/src/components/Admin/MemberReview/MemberReview.tsx b/frontend/src/components/Admin/MemberReview/MemberReview.tsx index b654dce00..ad797004b 100644 --- a/frontend/src/components/Admin/MemberReview/MemberReview.tsx +++ b/frontend/src/components/Admin/MemberReview/MemberReview.tsx @@ -65,7 +65,7 @@ const MemberReview: React.FC = () => { .map((it) => it.email); const sendReviewRequest = async () => { - await APIWrapper.post(`${backendURL}/reviewMemberDiffs`, { + await APIWrapper.put(`${backendURL}/memberDiffs`, { approved: approvedEmails, rejected: rejectedEmails }); diff --git a/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx b/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx index 35c6262b0..c1512af77 100644 --- a/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx +++ b/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx @@ -21,7 +21,7 @@ const TeamEventCreditForm: React.FC = () => { useEffect(() => { TeamEventsAPI.getAllTeamEventInfo().then((teamEvents) => setTeamEventInfoList(teamEvents)); - TeamEventsAPI.getTeamEventAttendanceByUser().then((attendance) => { + TeamEventsAPI.getTeamEventAttendanceByUser(userInfo.email).then((attendance) => { setApprovedAttendance(attendance.filter((attendee) => attendee.pending === false)); setPendingAttendance(attendance.filter((attendee) => attendee.pending === true)); setIsAttendanceLoading(false); diff --git a/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx b/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx index 683d1ddc9..3bf9f1fcb 100644 --- a/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx +++ b/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx @@ -3,6 +3,7 @@ import { Card, Image, Button, Modal } from 'semantic-ui-react'; import AvatarEditor from 'react-avatar-editor'; import ProfileImageEditor from './ProfileImageEditor'; import ImagesAPI from '../../../API/ImagesAPI'; +import { useSelf } from '../../Common/FirestoreDataProvider'; const UserProfileImage: React.FC = () => { const [open, setOpen] = React.useState(false); @@ -11,14 +12,16 @@ const UserProfileImage: React.FC = () => { const [editor, setEditor] = useState(null); const setEditorRef = (editor: AvatarEditor) => setEditor(editor); + const userInfo = useSelf(); + useEffect(() => { if (process.env.NODE_ENV === 'test') { return; } - ImagesAPI.getMemberImage().then((url: string) => { + ImagesAPI.getMemberImage(userInfo ? userInfo.email : '').then((url: string) => { setProfilePhoto(url); }); - }, []); + }, [userInfo]); const cropAndSubmitImage = () => { if (editor !== null) { @@ -28,7 +31,7 @@ const UserProfileImage: React.FC = () => { .then((res) => res.blob()) .then((blob) => { imageURL = window.URL.createObjectURL(blob); - ImagesAPI.uploadMemberImage(blob); + ImagesAPI.uploadMemberImage(blob, userInfo?.email || ''); setProfilePhoto(imageURL); }); }