From 221f7474087f709b7fb286db0345458aef0e34fe Mon Sep 17 00:00:00 2001 From: jacoblurie29 Date: Sat, 6 Apr 2024 17:08:51 -0500 Subject: [PATCH] Auth added to all endpoints --- src/config/server.config.ts | 10 ++++- src/controllers/User.controller.ts | 61 +++++++++++++++++++++++++++--- src/middleware/validateAuth.ts | 40 ++++++++++++++++++++ src/routes/routes.ts | 58 ++++++++++++++++------------ 4 files changed, 136 insertions(+), 33 deletions(-) create mode 100644 src/middleware/validateAuth.ts diff --git a/src/config/server.config.ts b/src/config/server.config.ts index c10cefc..7a6ff6b 100644 --- a/src/config/server.config.ts +++ b/src/config/server.config.ts @@ -1,8 +1,9 @@ -import routes from '../routes/routes'; +import router from '../routes/routes'; import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; +import type { Request, Response } from 'express'; export const configureServer = () => { const app = express(); @@ -21,7 +22,12 @@ export const configureServer = () => { } // Routes - routes(app); + app.use(router); + + // 404 - No route found + router.use((_: Request, res: Response) => { + res.status(404).send('404: Page not found'); + }); // Create the server return app; diff --git a/src/controllers/User.controller.ts b/src/controllers/User.controller.ts index 2e139c8..e4b135a 100644 --- a/src/controllers/User.controller.ts +++ b/src/controllers/User.controller.ts @@ -1,11 +1,13 @@ import { trimPassenger } from '../util/trim'; import Airtable from 'airtable'; import Joi from 'joi'; +import clerkClient from '@clerk/clerk-sdk-node'; +import type { WithAuthProp } from '@clerk/clerk-sdk-node'; import type { Request, Response } from 'express'; import type { PassengerData } from '../interfaces/passenger/passenger.interface'; /** - * This function returns all passengers connected to a user + * This function returns user data if it exists in the database * * Steps to complete: * 1. Get the first name, last name, and birthdate from the request body, if it doesn't exist return a 400 @@ -17,7 +19,7 @@ import type { PassengerData } from '../interfaces/passenger/passenger.interface' * @param req - the request object * @param res - the response object */ -export const createUser = async (req: Request, res: Response) => { +export const createUser = async (req: WithAuthProp, res: Response) => { // given a first name, last name, and birthdate, check if a user exists in the database const schema = Joi.object({ firstName: Joi.string().required(), @@ -34,10 +36,12 @@ export const createUser = async (req: Request, res: Response) => { return; } - // Format it like this: Cardenas, Jessica | 1989-11-10, birthday is a javascript date object - const formattedUserId = `${req.body.lastName}, ${req.body.firstName} | ${ - req.body.birthdate.split('T')[0] - }`; + // birthdate is formatted MM-DD-YYYY, change it to YYYY-MM-DD + const newBirthdateParts = req.body.birthdate.split('-'); + const newBirthdate = `${newBirthdateParts[2]}-${newBirthdateParts[0]}-${newBirthdateParts[1]}`; + + // Format it like this: Cardenas, Jessica | 1989-11-10, + const formattedUserId = `${req.body.lastName}, ${req.body.firstName} | ${newBirthdate}`; const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY || '', @@ -54,7 +58,52 @@ export const createUser = async (req: Request, res: Response) => { return res.status(400).send('User does not exist'); } + if (process.env.ENVIRONMENT !== 'test') { + try { + await clerkClient.users.updateUserMetadata(req.auth.userId || '', { + publicMetadata: { + onboardComplete: false, + }, + }); + } catch (error) { + return res.status(500).send('Error updating user metadata'); + } + } + return res .status(200) .send(trimPassenger(passenger[0] as unknown as PassengerData)); }; + +/** + * Links a user to an Airtable record. + * + * @param req - The request object containing the user's authentication information. + * @param res - The response object used to send the result of the operation. + * @returns A response indicating whether the user was successfully linked to the Airtable record. + */ +export const linkUserToAirtableRecord = async ( + req: WithAuthProp, + res: Response +) => { + const { airtableRecordId } = req.body; + + if (!airtableRecordId) { + return res.status(400).send('Airtable ID is required'); + } + + if (process.env.ENVIRONMENT !== 'test') { + try { + await clerkClient.users.updateUserMetadata(req.auth.userId || '', { + publicMetadata: { + airtableRecordId, + onboardComplete: true, + }, + }); + } catch (error) { + return res.status(500).send('Error updating user metadata'); + } + } + + return res.status(200).send('User linked to Airtable record'); +}; diff --git a/src/middleware/validateAuth.ts b/src/middleware/validateAuth.ts new file mode 100644 index 0000000..2ecf7d9 --- /dev/null +++ b/src/middleware/validateAuth.ts @@ -0,0 +1,40 @@ +import { ClerkExpressWithAuth } from '@clerk/clerk-sdk-node'; +import dotenv from 'dotenv'; +import type { WithAuthProp } from '@clerk/clerk-sdk-node'; +import type { Request, Response, NextFunction } from 'express'; +dotenv.config(); + +/** + * Middleware function to validate user authentication. + * If the environment is not test, it uses ClerkExpressWithAuth to validate the user's session. + * If the user is authenticated, it calls the next middleware function. + * If the user is not authenticated, it returns a 403 status code. + * If the environment is test, it calls the next middleware function without authentication. + * + * @param req - The Express request object. + * @param res - The Express response object. + * @param next - The next middleware function. + */ +const validateAuth = ( + req: WithAuthProp, + res: Response, + next: NextFunction +) => { + // If the environment is not test, use ClerkExpressWithAuth to validate the user's session + if (process.env.ENVIRONMENT !== 'test') { + // Use ClerkExpressWithAuth to validate the user's session then call next() if the user is authenticated + ClerkExpressWithAuth({})(req, res, async () => { + if (req.auth.sessionId && req.auth.userId) { + return next(); + } + + // If the user is not authenticated, return a 403 status code + return res.status(401).send('Unauthorized'); + }); + } else { + // If the environment is test, call next() to continue (no authentication is required in test environment) + return next(); + } +}; + +export default validateAuth; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 9b1e4a7..1ade2a2 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -11,35 +11,43 @@ import { updateFlightRequest, getFlightLegsById, } from '../controllers/FlightRequest.controller'; -import { createUser } from '../controllers/User.controller'; -import type { Express, Request, Response } from 'express'; +import { + createUser, + linkUserToAirtableRecord, +} from '../controllers/User.controller'; +import validateAuth from '../middleware/validateAuth'; +import express from 'express'; +import type { LooseAuthProp } from '@clerk/clerk-sdk-node'; +import type { Request, Response } from 'express'; -const routes = (app: Express) => { - // healthcheck - app.get('/healthcheck', (_: Request, res: Response) => res.sendStatus(200)); +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + interface Request extends LooseAuthProp {} + } +} - /* User Controller */ - app.post('/user/', createUser); +// Protected routes (require authentication) +const router = express.Router(); - /* Passenger Controller Routes */ - app.get('/passenger/accompanying', getAllPassengersForUser); - app.get('/passenger/:id', getPassengerById); - app.post('/passenger/:id', createPassenger); - app.put('/passenger/:id', updatePassenger); +// healthcheck +router.get('/healthcheck', (_: Request, res: Response) => res.sendStatus(200)); - /* Flight Request Controller Routes */ - app.get('/requests/', getAllFlightRequestsForUser); - app.get('/requests/:id', getFlightRequestById); - app.get('/requests/:id/legs', getFlightLegsById); - app.post('/requests/', createFlightRequest); - app.put('/requests/:id', updateFlightRequest); +/* User Controller */ +router.post('/user/', validateAuth, createUser); +router.post('/user/link', validateAuth, linkUserToAirtableRecord); - // 404 - app.use((_: Request, res: Response) => { - res.status(404).send('404: Page not found'); - }); +/* Passenger Controller Routes */ +router.get('/passenger/accompanying', validateAuth, getAllPassengersForUser); +router.get('/passenger/:id', validateAuth, getPassengerById); +router.post('/passenger/:id', validateAuth, createPassenger); +router.put('/passenger/:id', validateAuth, updatePassenger); - return app; -}; +/* Flight Request Controller Routes */ +router.get('/requests/', validateAuth, getAllFlightRequestsForUser); +router.get('/requests/:id', validateAuth, getFlightRequestById); +router.get('/requests/:id/legs', validateAuth, getFlightLegsById); +router.post('/requests/', validateAuth, createFlightRequest); +router.put('/requests/:id', validateAuth, updateFlightRequest); -export default routes; +export default router;