From 7c4e8d7db8bdb5de7443cc01745e8b22f6b83fa3 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sat, 13 Dec 2025 14:25:39 -0500 Subject: [PATCH 1/4] Implemented login requirement for most of the API --- api/src/middleware/auth.ts | 14 ++++++++++++++ api/src/routes/applications.ts | 19 ++++++++++--------- api/src/routes/auth.js | 3 ++- api/src/routes/calendar.ts | 11 ++++++----- api/src/routes/course.ts | 4 ++++ api/src/routes/loa.ts | 3 +++ api/src/routes/members.js | 7 ++----- api/src/routes/ranks.js | 9 +++++++-- api/src/routes/roles.js | 4 ++++ api/src/routes/statuses.js | 4 ++++ 10 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 api/src/middleware/auth.ts diff --git a/api/src/middleware/auth.ts b/api/src/middleware/auth.ts new file mode 100644 index 0000000..371acb7 --- /dev/null +++ b/api/src/middleware/auth.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from "express"; + +export const requireLogin = function (req: Request, res: Response, next: NextFunction) { + if (req.user?.id) + next(); + else + res.sendStatus(401) +} + +function requireRole(roleName: string) { + return function (req: Request, res: Response, next: NextFunction) { + + } +} \ No newline at end of file diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index e1c9394..585f199 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -9,6 +9,7 @@ import { ApplicationFull, CommentRow } from "@app/shared/types/application" import { assignUserToStatus } from '../services/statusService'; import { Request, response, Response } from 'express'; import { getUserRoles } from '../services/rolesService'; +import { requireLogin } from '../middleware/auth'; //get CoC router.get('/coc', async (req: Request, res: Response) => { @@ -29,7 +30,7 @@ router.get('/coc', async (req: Request, res: Response) => { // POST /application -router.post('/', async (req, res) => { +router.post('/', [requireLogin], async (req, res) => { try { const App = req.body?.App || {}; const memberID = req.user.id; @@ -47,7 +48,7 @@ router.post('/', async (req, res) => { }); // GET /application/all -router.get('/all', async (req, res) => { +router.get('/all', [requireLogin], async (req, res) => { try { const rows = await getApplicationList(); res.status(200).json(rows); @@ -71,7 +72,7 @@ router.get('/meList', async (req, res) => { } }) -router.get('/me', async (req, res) => { +router.get('/me', [requireLogin], async (req, res) => { let userID = req.user.id; @@ -96,7 +97,7 @@ router.get('/me', async (req, res) => { }) // GET /application/:id -router.get('/me/:id', async (req: Request, res: Response) => { +router.get('/me/:id', [requireLogin], async (req: Request, res: Response) => { let appID = Number(req.params.id); let member = req.user.id; try { @@ -123,7 +124,7 @@ router.get('/me/:id', async (req: Request, res: Response) => { }); // GET /application/:id -router.get('/:id', async (req: Request, res: Response) => { +router.get('/:id', [requireLogin], async (req: Request, res: Response) => { let appID = Number(req.params.id); let asAdmin = !!req.query.admin || false; let user = req.user.id; @@ -159,7 +160,7 @@ router.get('/:id', async (req: Request, res: Response) => { }); // POST /application/approve/:id -router.post('/approve/:id', async (req: Request, res: Response) => { +router.post('/approve/:id', [requireLogin], async (req: Request, res: Response) => { const appID = Number(req.params.id); const approved_by = req.user.id; @@ -188,7 +189,7 @@ router.post('/approve/:id', async (req: Request, res: Response) => { }); // POST /application/deny/:id -router.post('/deny/:id', async (req, res) => { +router.post('/deny/:id', [requireLogin], async (req, res) => { const appID = req.params.id; try { @@ -203,7 +204,7 @@ router.post('/deny/:id', async (req, res) => { }); // POST /application/:id/comment -router.post('/:id/comment', async (req: Request, res: Response) => { +router.post('/:id/comment', [requireLogin], async (req: Request, res: Response) => { const appID = req.params.id; const data = req.body.message; const user = req.user; @@ -246,7 +247,7 @@ VALUES(?, ?, ?);` }); // POST /application/:id/comment -router.post('/:id/adminComment', async (req: Request, res: Response) => { +router.post('/:id/adminComment', [requireLogin], async (req: Request, res: Response) => { const appID = req.params.id; const data = req.body.message; const user = req.user; diff --git a/api/src/routes/auth.js b/api/src/routes/auth.js index c736f33..8b0c379 100644 --- a/api/src/routes/auth.js +++ b/api/src/routes/auth.js @@ -7,6 +7,7 @@ const express = require('express'); const { param } = require('./applications'); const router = express.Router(); import pool from '../db'; +import { requireLogin } from '../middleware/auth'; const querystring = require('querystring'); @@ -90,7 +91,7 @@ router.get('/callback', (req, res, next) => { })(req, res, next); }); -router.get('/logout', function (req, res, next) { +router.get('/logout', [requireLogin], function (req, res, next) { req.logout(function (err) { if (err) { return next(err); } var params = { diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index ff486cb..3b9f119 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { createEvent, getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus, setEventCancelled, updateEvent } from "../services/calendarService"; import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar"; +import { requireLogin } from "../middleware/auth"; const express = require('express'); const r = express.Router(); @@ -35,7 +36,7 @@ r.get('/upcoming', async (req, res) => { res.sendStatus(501); }) -r.post('/:id/cancel', async (req: Request, res: Response) => { +r.post('/:id/cancel', [requireLogin], async (req: Request, res: Response) => { try { const eventID = Number(req.params.id); setEventCancelled(eventID, true); @@ -45,7 +46,7 @@ r.post('/:id/cancel', async (req: Request, res: Response) => { res.status(500).send('Error setting cancel status'); } }) -r.post('/:id/uncancel', async (req: Request, res: Response) => { +r.post('/:id/uncancel', [requireLogin], async (req: Request, res: Response) => { try { const eventID = Number(req.params.id); setEventCancelled(eventID, false); @@ -57,7 +58,7 @@ r.post('/:id/uncancel', async (req: Request, res: Response) => { }) -r.post('/:id/attendance', async (req: Request, res: Response) => { +r.post('/:id/attendance', [requireLogin], async (req: Request, res: Response) => { try { let member = req.user.id; let event = Number(req.params.id); @@ -85,7 +86,7 @@ r.get('/:id', async (req: Request, res: Response) => { //post a new calendar event -r.post('/', async (req: Request, res: Response) => { +r.post('/', [requireLogin], async (req: Request, res: Response) => { try { const member = req.user.id; let event: CalendarEvent = req.body; @@ -100,7 +101,7 @@ r.post('/', async (req: Request, res: Response) => { } }) -r.put('/', async (req: Request, res: Response) => { +r.put('/', [requireLogin], async (req: Request, res: Response) => { try { let event: CalendarEvent = req.body; event.start = new Date(event.start); diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 046b9bb..e1a9103 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -1,10 +1,14 @@ import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course"; import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent } from "../services/CourseSerivce"; import { Request, Response, Router } from "express"; +import { requireLogin } from "../middleware/auth"; const courseRouter = Router(); const eventRouter = Router(); +courseRouter.use(requireLogin) +eventRouter.use(requireLogin) + courseRouter.get('/', async (req, res) => { try { const courses = await getAllCourses(); diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index cbcc30b..d86bf47 100644 --- a/api/src/routes/loa.ts +++ b/api/src/routes/loa.ts @@ -5,6 +5,9 @@ import { Request, Response } from 'express'; import pool from '../db'; import { closeLOA, createNewLOA, getAllLOA, getLOAbyID, getLoaTypes, getUserLOA, setLOAExtension } from '../services/loaService'; import { LOARequest } from '@app/shared/types/loa'; +import { requireLogin } from '../middleware/auth'; + +router.use(requireLogin); //member posts LOA router.post("/", async (req: Request, res: Response) => { diff --git a/api/src/routes/members.js b/api/src/routes/members.js index 08c169e..cb16001 100644 --- a/api/src/routes/members.js +++ b/api/src/routes/members.js @@ -2,15 +2,12 @@ const express = require('express'); const router = express.Router(); import pool from '../db'; +import { requireLogin } from '../middleware/auth'; import { getUserActiveLOA } from '../services/loaService'; import { getUserData } from '../services/memberService'; import { getUserRoles } from '../services/rolesService'; -router.use((req, res, next) => { - console.log(req.user); - console.log('Time:', Date.now()) - next() -}) +router.use(requireLogin); //get all users router.get('/', async (req, res) => { diff --git a/api/src/routes/ranks.js b/api/src/routes/ranks.js index cb8c4b1..75e48b7 100644 --- a/api/src/routes/ranks.js +++ b/api/src/routes/ranks.js @@ -1,10 +1,15 @@ const express = require('express'); const r = express.Router(); const ur = express.Router(); -const { getAllRanks, insertMemberRank } = require('../services/rankService') +const { getAllRanks, insertMemberRank } = require('../services/rankService'); +const { requireLogin } = require('../middleware/auth'); + +r.use(requireLogin) +ur.use(requireLogin) //insert a new latest rank for a user -ur.post('/', async (req, res) => {3 +ur.post('/', async (req, res) => { + 3 try { const change = req.body?.change; await insertMemberRank(change.member_id, change.rank_id, change.date); diff --git a/api/src/routes/roles.js b/api/src/routes/roles.js index 2a435f0..332d140 100644 --- a/api/src/routes/roles.js +++ b/api/src/routes/roles.js @@ -3,8 +3,12 @@ const r = express.Router(); const ur = express.Router(); import pool from '../db'; +import { requireLogin } from '../middleware/auth'; import { assignUserGroup, createGroup } from '../services/rolesService'; +r.use(requireLogin) +ur.use(requireLogin) + //manually assign a member to a group ur.post('/', async (req, res) => { try { diff --git a/api/src/routes/statuses.js b/api/src/routes/statuses.js index 8e9d48e..7d07a71 100644 --- a/api/src/routes/statuses.js +++ b/api/src/routes/statuses.js @@ -3,6 +3,10 @@ const status = express.Router(); const memberStatus = express.Router(); import pool from '../db'; +import { requireLogin } from '../middleware/auth'; + +status.use(requireLogin); +memberStatus.use(requireLogin); //insert a new latest rank for a user memberStatus.post('/', async (req, res) => { From b91ecacb60fc1c2305da868de781da0fdb6da7d2 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sat, 13 Dec 2025 17:01:50 -0500 Subject: [PATCH 2/4] implemented role and state based authorization --- api/src/middleware/auth.ts | 39 +++++++++++++++++++++++-- api/src/routes/applications.ts | 24 ++++----------- api/src/routes/{auth.js => auth.ts} | 45 +++++++++++++++++++---------- api/src/routes/loa.ts | 10 +++---- api/src/routes/members.js | 6 ++-- api/src/services/memberService.ts | 15 ++++------ ui/src/api/application.ts | 4 ++- ui/src/api/trainingReport.ts | 18 ++++++++---- 8 files changed, 101 insertions(+), 60 deletions(-) rename api/src/routes/{auth.js => auth.ts} (76%) diff --git a/api/src/middleware/auth.ts b/api/src/middleware/auth.ts index 371acb7..19240ab 100644 --- a/api/src/middleware/auth.ts +++ b/api/src/middleware/auth.ts @@ -1,4 +1,6 @@ import { NextFunction, Request, Response } from "express"; +import { MemberState } from "../services/memberService"; +import { stat } from "fs"; export const requireLogin = function (req: Request, res: Response, next: NextFunction) { if (req.user?.id) @@ -7,8 +9,41 @@ export const requireLogin = function (req: Request, res: Response, next: NextFun res.sendStatus(401) } -function requireRole(roleName: string) { +export function requireMemberState(state: MemberState) { return function (req: Request, res: Response, next: NextFunction) { - + if (req.user?.state === state) + next(); + else + res.status(403).send("You must be a member of the 17th RBN to access this resource"); } +} + +export function requireRole(requiredRoles: string | string[]) { + // Normalize the input to always be an array of lowercase required roles + const normalizedRequiredRoles: string[] = Array.isArray(requiredRoles) + ? requiredRoles.map(role => role.toLowerCase()) + : [requiredRoles.toLowerCase()]; + + const DEV_ROLE = 'dev'; + + return function (req: Request, res: Response, next: NextFunction) { + if (!req.user || !req.user.roles) { + // User is not authenticated or has no roles array + return res.sendStatus(401); + } + + const userRolesLowercase = req.user.roles.map(role => role.name.toLowerCase()); + + // Check if the user has *any* of the required roles OR the 'dev' role + const hasAccess = userRolesLowercase.some(userRole => + userRole === DEV_ROLE || normalizedRequiredRoles.includes(userRole) + ); + + if (hasAccess) { + return next(); + } else { + // User is authenticated but does not have the necessary permissions + return res.sendStatus(403); + } + }; } \ No newline at end of file diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index 585f199..412fb2c 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -9,7 +9,7 @@ import { ApplicationFull, CommentRow } from "@app/shared/types/application" import { assignUserToStatus } from '../services/statusService'; import { Request, response, Response } from 'express'; import { getUserRoles } from '../services/rolesService'; -import { requireLogin } from '../middleware/auth'; +import { requireLogin, requireRole } from '../middleware/auth'; //get CoC router.get('/coc', async (req: Request, res: Response) => { @@ -48,7 +48,7 @@ router.post('/', [requireLogin], async (req, res) => { }); // GET /application/all -router.get('/all', [requireLogin], async (req, res) => { +router.get('/all', [requireLogin, requireRole("Recruiter")], async (req, res) => { try { const rows = await getApplicationList(); res.status(200).json(rows); @@ -124,22 +124,10 @@ router.get('/me/:id', [requireLogin], async (req: Request, res: Response) => { }); // GET /application/:id -router.get('/:id', [requireLogin], async (req: Request, res: Response) => { +router.get('/:id', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { let appID = Number(req.params.id); let asAdmin = !!req.query.admin || false; - let user = req.user.id; - //TODO: Replace this with bigger authorization system eventually - if (asAdmin) { - let allowed = (await getUserRoles(user)).some((role) => - role.name.toLowerCase() === 'dev' || - role.name.toLowerCase() === 'recruiter' || - role.name.toLowerCase() === 'administrator') - console.log(allowed) - if (!allowed) { - return res.sendStatus(403) - } - } try { const application = await getApplicationByID(appID); if (application === undefined) @@ -160,7 +148,7 @@ router.get('/:id', [requireLogin], async (req: Request, res: Response) => { }); // POST /application/approve/:id -router.post('/approve/:id', [requireLogin], async (req: Request, res: Response) => { +router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { const appID = Number(req.params.id); const approved_by = req.user.id; @@ -189,7 +177,7 @@ router.post('/approve/:id', [requireLogin], async (req: Request, res: Response) }); // POST /application/deny/:id -router.post('/deny/:id', [requireLogin], async (req, res) => { +router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req, res) => { const appID = req.params.id; try { @@ -247,7 +235,7 @@ VALUES(?, ?, ?);` }); // POST /application/:id/comment -router.post('/:id/adminComment', [requireLogin], async (req: Request, res: Response) => { +router.post('/:id/adminComment', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { const appID = req.params.id; const data = req.body.message; const user = req.user; diff --git a/api/src/routes/auth.js b/api/src/routes/auth.ts similarity index 76% rename from api/src/routes/auth.js rename to api/src/routes/auth.ts index 8b0c379..dafc2e0 100644 --- a/api/src/routes/auth.js +++ b/api/src/routes/auth.ts @@ -6,8 +6,11 @@ dotenv.config(); const express = require('express'); const { param } = require('./applications'); const router = express.Router(); +import { Role } from '@app/shared/types/roles'; import pool from '../db'; import { requireLogin } from '../middleware/auth'; +import { getUserRoles } from '../services/rolesService'; +import { getUserState, MemberState } from '../services/memberService'; const querystring = require('querystring'); @@ -22,13 +25,13 @@ passport.use(new OpenIDConnectStrategy({ scope: ['openid', 'profile'] }, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { - console.log('--- OIDC verify() called ---'); - console.log('issuer:', issuer); - console.log('sub:', sub); - // console.log('profile:', JSON.stringify(profile, null, 2)); - console.log('profile:', profile); - console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); - console.log('preferred_username:', jwtClaims?.preferred_username); + // console.log('--- OIDC verify() called ---'); + // console.log('issuer:', issuer); + // console.log('sub:', sub); + // // console.log('profile:', JSON.stringify(profile, null, 2)); + // console.log('profile:', profile); + // console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); + // console.log('preferred_username:', jwtClaims?.preferred_username); const con = await pool.getConnection(); try { @@ -67,12 +70,6 @@ router.get('/login', (req, res, next) => { next(); }, passport.authenticate('openidconnect')); -// router.get('/callback', (req, res, next) => { -// passport.authenticate('openidconnect', { -// successRedirect: req.session.redirectTo, -// failureRedirect: process.env.CLIENT_URL -// }) -// }); router.get('/callback', (req, res, next) => { const redirectURI = req.session.redirectTo; @@ -111,15 +108,17 @@ passport.serializeUser(function (user, cb) { passport.deserializeUser(function (user, cb) { process.nextTick(async function () { - const memberID = user.memberId; + const memberID = user.memberId as number; const con = await pool.getConnection(); - var userData; + var userData: { id: number, name: string, roles: Role[], state: MemberState }; try { let userResults = await con.query(`SELECT id, name FROM members WHERE id = ?;`, [memberID]) userData = userResults[0]; - + let userRoles = await getUserRoles(memberID); + userData.roles = userRoles; + userData.state = await getUserState(memberID); } catch (error) { console.error(error) } finally { @@ -129,5 +128,19 @@ passport.deserializeUser(function (user, cb) { }); }); +declare global { + namespace Express { + interface Request { + user: { + id: number; + name: string; + roles: Role[]; + state: MemberState; + }; + } + } +} + + module.exports = router; \ No newline at end of file diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index d86bf47..6c984b1 100644 --- a/api/src/routes/loa.ts +++ b/api/src/routes/loa.ts @@ -5,7 +5,7 @@ import { Request, Response } from 'express'; import pool from '../db'; import { closeLOA, createNewLOA, getAllLOA, getLOAbyID, getLoaTypes, getUserLOA, setLOAExtension } from '../services/loaService'; import { LOARequest } from '@app/shared/types/loa'; -import { requireLogin } from '../middleware/auth'; +import { requireLogin, requireRole } from '../middleware/auth'; router.use(requireLogin); @@ -26,7 +26,7 @@ router.post("/", async (req: Request, res: Response) => { }); //admin posts LOA -router.post("/admin", async (req: Request, res: Response) => { +router.post("/admin", [requireRole("17th Administrator")], async (req: Request, res: Response) => { let LOARequest = req.body as LOARequest; LOARequest.created_by = req.user.id; LOARequest.filed_date = new Date(); @@ -66,7 +66,7 @@ router.get("/history", async (req: Request, res: Response) => { } }) -router.get('/all', async (req, res) => { +router.get('/all', [requireRole("17th Administrator")], async (req, res) => { try { const result = await getAllLOA(); res.status(200).json(result) @@ -104,7 +104,7 @@ router.post('/cancel/:id', async (req: Request, res: Response) => { }) //TODO: enforce admin only -router.post('/adminCancel/:id', async (req: Request, res: Response) => { +router.post('/adminCancel/:id', [requireRole("17th Administrator")], async (req: Request, res: Response) => { let closer = req.user.id; try { await closeLOA(Number(req.params.id), closer); @@ -116,7 +116,7 @@ router.post('/adminCancel/:id', async (req: Request, res: Response) => { }) // TODO: Enforce admin only -router.post('/extend/:id', async (req: Request, res: Response) => { +router.post('/extend/:id', [requireRole("17th Administrator")], async (req: Request, res: Response) => { const to: Date = req.body.to; if (!to) { diff --git a/api/src/routes/members.js b/api/src/routes/members.js index cb16001..24b75c3 100644 --- a/api/src/routes/members.js +++ b/api/src/routes/members.js @@ -2,15 +2,15 @@ const express = require('express'); const router = express.Router(); import pool from '../db'; -import { requireLogin } from '../middleware/auth'; +import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; import { getUserActiveLOA } from '../services/loaService'; -import { getUserData } from '../services/memberService'; +import { getUserData, MemberState } from '../services/memberService'; import { getUserRoles } from '../services/rolesService'; router.use(requireLogin); //get all users -router.get('/', async (req, res) => { +router.get('/', [requireMemberState(MemberState.Member)], async (req, res) => { try { const result = await pool.query( `SELECT diff --git a/api/src/services/memberService.ts b/api/src/services/memberService.ts index 4563a6a..8a467dd 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/memberService.ts @@ -22,13 +22,8 @@ export async function setUserState(userID: number, state: MemberState) { return await pool.query(sql, [state, userID]); } -declare global { - namespace Express { - interface Request { - user: { - id: number; - name: string; - }; - } - } -} +export async function getUserState(user: number): Promise { + let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]); + console.log('hi') + return (out[0].state as MemberState); +} \ No newline at end of file diff --git a/ui/src/api/application.ts b/ui/src/api/application.ts index ec05982..f814b15 100644 --- a/ui/src/api/application.ts +++ b/ui/src/api/application.ts @@ -59,7 +59,9 @@ export async function postAdminChatMessage(message: any, post_id: number) { } export async function getAllApplications(): Promise { - const res = await fetch(`${addr}/application/all`) + const res = await fetch(`${addr}/application/all`, { + credentials: 'include', + }) if (res.ok) { return res.json() diff --git a/ui/src/api/trainingReport.ts b/ui/src/api/trainingReport.ts index a7a04b3..c1b8adc 100644 --- a/ui/src/api/trainingReport.ts +++ b/ui/src/api/trainingReport.ts @@ -4,7 +4,9 @@ import { Course, CourseAttendeeRole, CourseEventDetails, CourseEventSummary } fr const addr = import.meta.env.VITE_APIHOST; export async function getTrainingReports(sortMode: string, search: string): Promise { - const res = await fetch(`${addr}/courseEvent?sort=${sortMode}&search=${search}`); + const res = await fetch(`${addr}/courseEvent?sort=${sortMode}&search=${search}`, { + credentials: 'include', + }); if (res.ok) { return await res.json() as Promise; @@ -15,7 +17,9 @@ export async function getTrainingReports(sortMode: string, search: string): Prom } export async function getTrainingReport(id: number): Promise { - const res = await fetch(`${addr}/courseEvent/${id}`); + const res = await fetch(`${addr}/courseEvent/${id}`, { + credentials: 'include', + }); if (res.ok) { return await res.json() as Promise; @@ -26,10 +30,12 @@ export async function getTrainingReport(id: number): Promise } export async function getAllTrainings(): Promise { - const res = await fetch(`${addr}/course`); + const res = await fetch(`${addr}/course`, { + credentials: 'include', + }); if (res.ok) { - return await res.json() as Promise; + return await res.json() as Promise; } else { console.error("Something went wrong"); throw new Error("Failed to load training list"); @@ -37,7 +43,9 @@ export async function getAllTrainings(): Promise { } export async function getAllAttendeeRoles(): Promise { - const res = await fetch(`${addr}/course/roles`); + const res = await fetch(`${addr}/course/roles`, { + credentials: 'include', + }); if (res.ok) { return await res.json() as Promise; From 92294758363ca7c7cffdfff783b022fcd76ff256 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 14 Dec 2025 11:38:45 -0500 Subject: [PATCH 3/4] finished state and role based auth across the full API --- api/src/middleware/auth.ts | 2 +- api/src/routes/calendar.ts | 13 +++++++------ api/src/routes/course.ts | 5 ++++- api/src/routes/members.js | 2 +- api/src/routes/ranks.js | 5 +++-- api/src/routes/roles.js | 13 +++++++------ 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/api/src/middleware/auth.ts b/api/src/middleware/auth.ts index 19240ab..395c200 100644 --- a/api/src/middleware/auth.ts +++ b/api/src/middleware/auth.ts @@ -14,7 +14,7 @@ export function requireMemberState(state: MemberState) { if (req.user?.state === state) next(); else - res.status(403).send("You must be a member of the 17th RBN to access this resource"); + res.status(403).send(`You must be a ${state} of the 17th RBN to access this resource`); } } diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index 3b9f119..d3f91d8 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -1,7 +1,8 @@ import { Request, Response } from "express"; import { createEvent, getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus, setEventCancelled, updateEvent } from "../services/calendarService"; import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar"; -import { requireLogin } from "../middleware/auth"; +import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; +import { MemberState } from "../services/memberService"; const express = require('express'); const r = express.Router(); @@ -36,7 +37,7 @@ r.get('/upcoming', async (req, res) => { res.sendStatus(501); }) -r.post('/:id/cancel', [requireLogin], async (req: Request, res: Response) => { +r.post('/:id/cancel', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { try { const eventID = Number(req.params.id); setEventCancelled(eventID, true); @@ -46,7 +47,7 @@ r.post('/:id/cancel', [requireLogin], async (req: Request, res: Response) => { res.status(500).send('Error setting cancel status'); } }) -r.post('/:id/uncancel', [requireLogin], async (req: Request, res: Response) => { +r.post('/:id/uncancel', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { try { const eventID = Number(req.params.id); setEventCancelled(eventID, false); @@ -58,7 +59,7 @@ r.post('/:id/uncancel', [requireLogin], async (req: Request, res: Response) => { }) -r.post('/:id/attendance', [requireLogin], async (req: Request, res: Response) => { +r.post('/:id/attendance', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { try { let member = req.user.id; let event = Number(req.params.id); @@ -86,7 +87,7 @@ r.get('/:id', async (req: Request, res: Response) => { //post a new calendar event -r.post('/', [requireLogin], async (req: Request, res: Response) => { +r.post('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { try { const member = req.user.id; let event: CalendarEvent = req.body; @@ -101,7 +102,7 @@ r.post('/', [requireLogin], async (req: Request, res: Response) => { } }) -r.put('/', [requireLogin], async (req: Request, res: Response) => { +r.put('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { try { let event: CalendarEvent = req.body; event.start = new Date(event.start); diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index e1a9103..6e86c23 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -1,13 +1,16 @@ import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course"; import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent } from "../services/CourseSerivce"; import { Request, Response, Router } from "express"; -import { requireLogin } from "../middleware/auth"; +import { requireLogin, requireMemberState } from "../middleware/auth"; +import { MemberState } from "../services/memberService"; const courseRouter = Router(); const eventRouter = Router(); courseRouter.use(requireLogin) eventRouter.use(requireLogin) +courseRouter.use(requireMemberState(MemberState.Member)) +eventRouter.use(requireMemberState(MemberState.Member)) courseRouter.get('/', async (req, res) => { try { diff --git a/api/src/routes/members.js b/api/src/routes/members.js index 24b75c3..207fd86 100644 --- a/api/src/routes/members.js +++ b/api/src/routes/members.js @@ -74,7 +74,7 @@ router.get('/:id', async (req, res) => { //update a user's display name (stub) router.put('/:id/displayname', async (req, res) => { // Stub: not implemented yet - return res.status(501).json({ error: 'Update display name not implemented' }); + return res.status(501); }); diff --git a/api/src/routes/ranks.js b/api/src/routes/ranks.js index 75e48b7..81ca611 100644 --- a/api/src/routes/ranks.js +++ b/api/src/routes/ranks.js @@ -2,13 +2,14 @@ const express = require('express'); const r = express.Router(); const ur = express.Router(); const { getAllRanks, insertMemberRank } = require('../services/rankService'); -const { requireLogin } = require('../middleware/auth'); +const { requireLogin, requireMemberState, requireRole } = require('../middleware/auth'); +const { MemberState } = require('../services/memberService'); r.use(requireLogin) ur.use(requireLogin) //insert a new latest rank for a user -ur.post('/', async (req, res) => { +ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]),requireMemberState(MemberState.Member)], async (req, res) => { 3 try { const change = req.body?.change; diff --git a/api/src/routes/roles.js b/api/src/routes/roles.js index 332d140..283c707 100644 --- a/api/src/routes/roles.js +++ b/api/src/routes/roles.js @@ -3,14 +3,15 @@ const r = express.Router(); const ur = express.Router(); import pool from '../db'; -import { requireLogin } from '../middleware/auth'; +import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; +import { MemberState } from '../services/memberService'; import { assignUserGroup, createGroup } from '../services/rolesService'; r.use(requireLogin) ur.use(requireLogin) //manually assign a member to a group -ur.post('/', async (req, res) => { +ur.post('/', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { try { const body = req.body; @@ -24,7 +25,7 @@ ur.post('/', async (req, res) => { }); //manually remove member from group -ur.delete('/', async (req, res) => { +ur.delete('/', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { try { const body = req.body; console.log(body); @@ -42,7 +43,7 @@ ur.delete('/', async (req, res) => { }) //get all roles -r.get('/', async (req, res) => { +r.get('/', [requireMemberState(MemberState.member)], async (req, res) => { try { const con = await pool.getConnection(); @@ -81,7 +82,7 @@ r.get('/', async (req, res) => { }); //create a new role -r.post('/', async (req, res) => { +r.post('/', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { try { const { name, color, description } = req.body; console.log('Creating role:', { name, color, description }); @@ -103,7 +104,7 @@ r.post('/', async (req, res) => { } }) -r.delete('/:id', async (req, res) => { +r.delete('/:id', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { try { const id = req.params.id; From af984cddbde5ce18652ad8dad1bc233f5b637dfb Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 14 Dec 2025 12:19:16 -0500 Subject: [PATCH 4/4] Updated references of moved things --- api/src/middleware/auth.ts | 2 +- api/src/routes/auth.ts | 3 ++- api/src/routes/calendar.ts | 2 +- api/src/routes/course.ts | 2 +- api/src/routes/members.ts | 2 +- api/src/routes/{ranks.js => ranks.ts} | 12 +++++++----- api/src/routes/{roles.js => roles.ts} | 12 ++++++------ ui/src/api/loa.ts | 2 ++ 8 files changed, 21 insertions(+), 16 deletions(-) rename api/src/routes/{ranks.js => ranks.ts} (67%) rename api/src/routes/{roles.js => roles.ts} (89%) diff --git a/api/src/middleware/auth.ts b/api/src/middleware/auth.ts index 395c200..e908b3e 100644 --- a/api/src/middleware/auth.ts +++ b/api/src/middleware/auth.ts @@ -1,5 +1,5 @@ +import { MemberState } from "@app/shared/types/member"; import { NextFunction, Request, Response } from "express"; -import { MemberState } from "../services/memberService"; import { stat } from "fs"; export const requireLogin = function (req: Request, res: Response, next: NextFunction) { diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index dafc2e0..ca8eeba 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -10,7 +10,8 @@ import { Role } from '@app/shared/types/roles'; import pool from '../db'; import { requireLogin } from '../middleware/auth'; import { getUserRoles } from '../services/rolesService'; -import { getUserState, MemberState } from '../services/memberService'; +import { getUserState } from '../services/memberService'; +import { MemberState } from '@app/shared/types/member'; const querystring = require('querystring'); diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index d3f91d8..82225ef 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { createEvent, getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus, setEventCancelled, updateEvent } from "../services/calendarService"; import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar"; import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; -import { MemberState } from "../services/memberService"; +import { MemberState } from "@app/shared/types/member"; const express = require('express'); const r = express.Router(); diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 6e86c23..35bf916 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -2,7 +2,7 @@ import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course"; import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent } from "../services/CourseSerivce"; import { Request, Response, Router } from "express"; import { requireLogin, requireMemberState } from "../middleware/auth"; -import { MemberState } from "../services/memberService"; +import { MemberState } from "@app/shared/types/member"; const courseRouter = Router(); const eventRouter = Router(); diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index a8aec37..f0f06db 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -7,7 +7,7 @@ import { requireLogin, requireMemberState, requireRole } from '../middleware/aut import { getUserActiveLOA } from '../services/loaService'; import { getMemberSettings, getMembersFull, getMembersLite, getUserData, setUserSettings } from '../services/memberService'; import { getUserRoles } from '../services/rolesService'; -import { MemberState } from '@app/shared/types/member'; +import { memberSettings, MemberState } from '@app/shared/types/member'; router.use(requireLogin); diff --git a/api/src/routes/ranks.js b/api/src/routes/ranks.ts similarity index 67% rename from api/src/routes/ranks.js rename to api/src/routes/ranks.ts index 81ca611..ee16d13 100644 --- a/api/src/routes/ranks.js +++ b/api/src/routes/ranks.ts @@ -1,15 +1,17 @@ -const express = require('express'); +import { MemberState } from "@app/shared/types/member"; +import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; +import { getAllRanks, insertMemberRank } from "../services/rankService"; + +import express = require('express'); const r = express.Router(); const ur = express.Router(); -const { getAllRanks, insertMemberRank } = require('../services/rankService'); -const { requireLogin, requireMemberState, requireRole } = require('../middleware/auth'); -const { MemberState } = require('../services/memberService'); + r.use(requireLogin) ur.use(requireLogin) //insert a new latest rank for a user -ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]),requireMemberState(MemberState.Member)], async (req, res) => { +ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req, res) => { 3 try { const change = req.body?.change; diff --git a/api/src/routes/roles.js b/api/src/routes/roles.ts similarity index 89% rename from api/src/routes/roles.js rename to api/src/routes/roles.ts index 283c707..e5429a8 100644 --- a/api/src/routes/roles.js +++ b/api/src/routes/roles.ts @@ -2,16 +2,16 @@ const express = require('express'); const r = express.Router(); const ur = express.Router(); +import { MemberState } from '@app/shared/types/member'; import pool from '../db'; import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; -import { MemberState } from '../services/memberService'; import { assignUserGroup, createGroup } from '../services/rolesService'; r.use(requireLogin) ur.use(requireLogin) //manually assign a member to a group -ur.post('/', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { +ur.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const body = req.body; @@ -25,7 +25,7 @@ ur.post('/', [requireMemberState(MemberState.member), requireRole("17th Administ }); //manually remove member from group -ur.delete('/', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { +ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const body = req.body; console.log(body); @@ -43,7 +43,7 @@ ur.delete('/', [requireMemberState(MemberState.member), requireRole("17th Admini }) //get all roles -r.get('/', [requireMemberState(MemberState.member)], async (req, res) => { +r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => { try { const con = await pool.getConnection(); @@ -82,7 +82,7 @@ r.get('/', [requireMemberState(MemberState.member)], async (req, res) => { }); //create a new role -r.post('/', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { +r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const { name, color, description } = req.body; console.log('Creating role:', { name, color, description }); @@ -104,7 +104,7 @@ r.post('/', [requireMemberState(MemberState.member), requireRole("17th Administr } }) -r.delete('/:id', [requireMemberState(MemberState.member), requireRole("17th Administrator")], async (req, res) => { +r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { try { const id = req.params.id; diff --git a/ui/src/api/loa.ts b/ui/src/api/loa.ts index dd7350a..5a356a0 100644 --- a/ui/src/api/loa.ts +++ b/ui/src/api/loa.ts @@ -43,6 +43,7 @@ export async function getMyLOA(): Promise { headers: { "Content-Type": "application/json", }, + credentials: 'include', }); @@ -63,6 +64,7 @@ export function getAllLOAs(): Promise { headers: { "Content-Type": "application/json", }, + credentials: 'include', }).then((res) => { if (res.ok) { return res.json();