Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /student endpoints #5

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
762d089
Add firebase-tools as dev-dependency
feverdreme Nov 12, 2023
c3213bf
Create /students api route handler
feverdreme Nov 12, 2023
6851600
Add route prototypes to /student from notion
feverdreme Nov 12, 2023
c1ac249
implemented /students/register
feverdreme Nov 12, 2023
638c1a9
Merge branch 'main' into studentsAPI
feverdreme Nov 15, 2023
5454633
Implemented new api routes
feverdreme Nov 18, 2023
b6a17ac
Migrate to new project architecture
feverdreme Nov 18, 2023
14f9be6
Change /getStudent to use queryParams
feverdreme Nov 18, 2023
edea880
Make error messages have error param
feverdreme Nov 18, 2023
3cb1897
Fix typo
feverdreme Nov 18, 2023
fa624d9
Create studentMutation interface
feverdreme Nov 29, 2023
4e580fb
ts-ignore fix later
feverdreme Nov 29, 2023
27dd46b
Change students endpoint to use `Router`
feverdreme Nov 29, 2023
65e3422
Revert "Add firebase-tools as dev-dependency"
feverdreme Nov 29, 2023
4d01310
Change endpoint names to root & add request verbs
feverdreme Nov 29, 2023
fbd73bc
Refactor code for students.get endpoint
feverdreme Nov 29, 2023
3ad99ab
Remove deprecated get verb
feverdreme Dec 6, 2023
e753b98
Implement delete action
feverdreme Dec 6, 2023
3964523
ts-ignore
feverdreme Dec 6, 2023
d56d553
Add error handling for firestore functions
feverdreme Dec 6, 2023
b7ed86e
Add check for existing student in POST verb
feverdreme Dec 6, 2023
ae03e50
Small comment and formatting fixes
feverdreme Dec 6, 2023
be58514
More formatting fixes
feverdreme Dec 6, 2023
e526bd9
Patch bug that allows id-email incongruity.
feverdreme Dec 6, 2023
6772b5c
Merge branch 'main' into studentsAPI
feverdreme Dec 6, 2023
429796d
Fix bugs with POST and GET validation/parsing
feverdreme Dec 6, 2023
ce4fcec
Add optional allergies field to student
feverdreme Dec 6, 2023
4a9529e
Add catchAll error handling wrapper
feverdreme Dec 6, 2023
80eb895
Merge branch 'main' into studentsAPI
feverdreme Jan 16, 2024
f505206
Format and move types to separate file
feverdreme Jan 16, 2024
e6f76d4
Minor formatting changes
feverdreme Jan 16, 2024
3dcb34d
Remove misc property
feverdreme Jan 16, 2024
9e130b2
Update firebase package.json
feverdreme Apr 13, 2024
ea7a822
Use automatic json middeware
feverdreme Apr 13, 2024
bf6916b
Fix /students GET operations with filters
feverdreme Apr 18, 2024
34d26eb
Include exception error messages in try blocks
feverdreme Apr 18, 2024
ee7806f
Add scaffolding for /teams. Implement POST
feverdreme Apr 18, 2024
ad9136b
Add email validation to /students
feverdreme Apr 20, 2024
31212a0
Fix email validation bug in PUT /students
feverdreme Apr 27, 2024
8acf121
Enforce `email` field to be in isEmailedStudnet
feverdreme Apr 27, 2024
fef94fa
Modify POST `/students` to accept `/:email`
feverdreme Apr 27, 2024
6df772b
Add school to student API
feverdreme Apr 27, 2024
6f4a619
Fix POST `/teams`
feverdreme Apr 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
},
"main": "lib/index.js",
"dependencies": {
"firebase": "^10.7.1",
"firebase-admin": "^11.8.0",
"firebase-functions": "^4.3.1"
"firebase-functions": "^4.6.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.12.0",
Expand Down
14 changes: 12 additions & 2 deletions backend/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ import * as functions from 'firebase-functions';
import admin from 'firebase-admin';
import express from 'express';
import cors from 'cors';
import ServiceAccount from '../service-account.json';

import students from './routes/students/index';
import teams from "./routes/teams/index"

//import bodyParser from 'body-parser';

const serviceAccount: string = require('../service-account.json');
// import ServiceAccount from '../service-account.json';

// initialize firebase in order to access its services
admin.initializeApp({ credential: admin.credential.cert(JSON.stringify(ServiceAccount)) });
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });

//initialize express server
const app = express();
app.use(express.json());
app.use(cors());

app.use("/students", students)
app.use("/teams", teams)

const db = admin.firestore();

exports.api = functions.https.onRequest(app);
Expand Down
164 changes: 164 additions & 0 deletions backend/functions/src/routes/students/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import express, { Request, Response } from 'express';
import { db } from '../../index';
// import { CollectionReference, Query } from 'firebase-admin/firestore';
import {student, isStudent} from "./types"
import { Query } from 'firebase-admin/database';
import { CollectionReference } from 'firebase-admin/firestore';

const students = express.Router();

// TODO: Convert to middleware
function catchAll(
func: (req: Request, res: Response) => Promise<void>
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response) => {
try {
await func(req, res);
} catch (e: any) {
res.status(500).send(e.message);
}
};
}

/**
* Creates a new student in the `students` collection using x-www-form-urlencoded data.
feverdreme marked this conversation as resolved.
Show resolved Hide resolved
*
* Upon success returns the document ID with HTTP 202
* Upon fail returns HTTP 400
*/
students.post('/:email', catchAll(async (req, res) => {
const email = req.params.email
let studentData = req.body;

// Validate form data
if (!isStudent(studentData) || email == "") {
res.status(400).send({
error:
'Malformed student registration request. Missing some required fields.',
});
return;
}

// Check email not in database already
try {
let docRef = await db.collection('students').doc(email).get();

if (docRef.exists) {
res.status(400).send({
error: `Student with email: ${email} already exists.`,
});
return;
}
} catch (e: any) {
res.status(500).send({ error: `Problem with email validation. ${e.message}` });
}

// Create student with email
try {
await db.collection('students').doc(email).set(studentData);
} catch (e: any) {
res.status(400).send({ error: `Could not create student. ${e.message}` });
return;
}

res.sendStatus(200);
}));

/**
* Updates a student with new parameters. Does not support updating arrays yet.
*/
students.put('/:email', catchAll(async (req, res) => {
let email = req.params.email;
let studentData = req.body;
feverdreme marked this conversation as resolved.
Show resolved Hide resolved

// Check that email field exists on req.body
if (email == "") {
res.status(404).send({
error: 'Student email not specified.',
});
return;
}

// Update the database
try {
await db
.collection('students')
.doc(email)
// @ts-ignore
.update(studentData);
} catch (e: any) {
res.status(404).send({
error: `Student with email: ${email} could not be updated. ${e.message}`,
});
return;
}

res.sendStatus(200);
}));

/**
* Retrives all students.
*/
students.get('/', catchAll(async (req, res) => {
let collectionRef: CollectionReference | Query = db.collection('students');

// Evaluate filters placed on GET
let filters: student = req.query;
Object.entries(filters).forEach(([key, value]) => {
// @ts-ignore
collectionRef = collectionRef.where(key, "==", value);
});

let snapshotRef = await collectionRef.get();

let addedData: Map<string, student> = new Map();
snapshotRef.forEach((doc) => {
addedData.set(doc.id, doc.data() as student);
});

res.status(200).send(Object.fromEntries(addedData));
}));

/**
* Retrieves student with specificed `email` param.
* Eg. GET /students/[email protected]
*/
students.get('/:email', catchAll(async (req, res) => {
let email = req.params.email;

let collectionRef = db.collection('students');
let docRef = await collectionRef.doc(email).get();

if (docRef.exists) {
res.status(200).send({[email]: docRef.data()});
} else {
res
.status(404)
.send({ error: `Could not get data from email: ${email}` });
}
}));

/**
* Deletes student from the students collection.
*/
students.delete('/:email', catchAll(async (req, res) => {
let email = req.params.email;
let collectionRef = db.collection('students');

// Handle when email not specified
if (email == '') {
res.status(400).send({ error: 'Email must be specified.' });
return;
}

try {
await collectionRef.doc(email).delete();
} catch (e: any) {
res.status(404).send({ error: `Cannot find email: ${email}. ${e.message}` });
return;
}

res.status(200).send(`Successfully deleted: ${email}`);
}));

export default students;
feverdreme marked this conversation as resolved.
Show resolved Hide resolved
24 changes: 24 additions & 0 deletions backend/functions/src/routes/students/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface student {
firstName?: string;
lastName?: string;
gradYear?: number;
netid?: string;
school?: string;
allergies?: string;
}

export type emailed_student = student & {email : string};

export function isStudent(data: object): data is emailed_student {
return (
'firstName' in data &&
'lastName' in data &&
'gradYear' in data &&
'netid' in data &&
'school' in data
);
}

export function isStudentMutation(data: object): data is student {
return !('email' in data) && Object.keys(data).length != 0;
}
107 changes: 107 additions & 0 deletions backend/functions/src/routes/teams/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import express, {Request, Response} from 'express';
import {db} from "../../index"
import {isTeam} from "./types"

const teams = express.Router()

function catchAll(
func: (req: Request, res: Response) => Promise<void>
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response) => {
try {
await func(req, res);
} catch (e: any) {
res.status(500).send(e.message);
}
};
}

/**
* Creates a new team
*
* Requires name attribute
* Requires owner attribute
*/
teams.post("/", catchAll(async (req, res) => {
const data = req.body;

// Validate form data
if (!isTeam(data)) {
res.status(400).send({
error: "Requested team must have name and owner attribute"
}); return;
}

// Check that owner exists in the database
let userRef = await db.collection('students').doc(data.name).get();
if (!userRef.exists) {
res.status(400).send({
error: 'User attempting to create team does not exist'
});
}

// Check for team existence
let docRef = await db.collection('teams').doc(data.name).get();

if (docRef.exists) {
res.status(400).send({
error: `Team with name: ${data.name} already exists.`
});
return;
}

// Attempt to create team
try {
await db
.collection('teams')
.doc(data.name)
.set({ owner: data.owner, members: [data.owner] });
} catch (e: any) {
res.status(400).send({ error: `Could not create team. ${e.message}` });
return;
}

res.sendStatus(200);
}))

/**
* Adds a member to a team
*/
teams.put(
'/member',
catchAll(async (req, res) => {

})
);

/**
* Changes the owner of a team. Can only be fulfilled by a team owner. Need auth to do this.
*/
teams.put(
'/changeOwner',
catchAll(async (req, res) => {})
);

/**
* Gets a team's data based off of its team. Requires auth again.
*/
teams.get(
'/',
catchAll(async (req, res) => {})
);

/**
* Deletes a team. Requires auth
*/
teams.delete("/", catchAll(async (req, res) => {

}))

/**
* Deletes a member from a team. Requires auth.
*/
teams.delete("/member", catchAll(async (req, res) => {

}))

export default teams
18 changes: 18 additions & 0 deletions backend/functions/src/routes/teams/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface team {
name: string;
owner: string;
}

export interface teamData {
name: string;
owner: string;
members: string[];
}

export function isTeam(t: object): t is team {
return "name" in t && "owner" in t;
}

export function isTeamData(t: object): t is teamData {
return isTeam(t) && "members" in t;
}
8 changes: 8 additions & 0 deletions frontend/src/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"apiKey": "AIzaSyD7QWAyZcNKCyG5HXLrHa7Gd3gHvjmuD3k",
"authDomain": "brh-registration.firebaseapp.com",
"projectId": "brh-registration",
"storageBucket": "brh-registration.appspot.com",
"messagingSenderId": "546493816978",
"appId": "1:546493816978:web:5d9acfebf78a2ee27d464b"
}