diff --git a/api/src/middleware/auth.ts b/api/src/middleware/auth.ts new file mode 100644 index 0000000..e908b3e --- /dev/null +++ b/api/src/middleware/auth.ts @@ -0,0 +1,49 @@ +import { MemberState } from "@app/shared/types/member"; +import { NextFunction, Request, Response } from "express"; +import { stat } from "fs"; + +export const requireLogin = function (req: Request, res: Response, next: NextFunction) { + if (req.user?.id) + next(); + else + res.sendStatus(401) +} + +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 ${state} 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 239c3dd..a479429 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -10,6 +10,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, requireRole } from '../middleware/auth'; //get CoC router.get('/coc', async (req: Request, res: Response) => { @@ -30,7 +31,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; @@ -48,7 +49,7 @@ router.post('/', async (req, res) => { }); // GET /application/all -router.get('/all', async (req, res) => { +router.get('/all', [requireLogin, requireRole("Recruiter")], async (req, res) => { try { const rows = await getApplicationList(); res.status(200).json(rows); @@ -72,7 +73,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; @@ -97,7 +98,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 { @@ -124,22 +125,10 @@ router.get('/me/:id', async (req: Request, res: Response) => { }); // GET /application/:id -router.get('/:id', 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 +149,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, requireRole("Recruiter")], async (req: Request, res: Response) => { const appID = Number(req.params.id); const approved_by = req.user.id; @@ -189,7 +178,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, requireRole("Recruiter")], async (req, res) => { const appID = req.params.id; try { @@ -204,7 +193,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; @@ -247,7 +236,7 @@ VALUES(?, ?, ?);` }); // POST /application/:id/comment -router.post('/:id/adminComment', 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 72% rename from api/src/routes/auth.js rename to api/src/routes/auth.ts index c736f33..ca8eeba 100644 --- a/api/src/routes/auth.js +++ b/api/src/routes/auth.ts @@ -6,7 +6,12 @@ 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 } from '../services/memberService'; +import { MemberState } from '@app/shared/types/member'; const querystring = require('querystring'); @@ -21,13 +26,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 { @@ -66,12 +71,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; @@ -90,7 +89,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 = { @@ -110,15 +109,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 { @@ -128,5 +129,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/calendar.ts b/api/src/routes/calendar.ts index ff486cb..82225ef 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -1,6 +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, requireMemberState, requireRole } from "../middleware/auth"; +import { MemberState } from "@app/shared/types/member"; const express = require('express'); const r = express.Router(); @@ -35,7 +37,7 @@ r.get('/upcoming', async (req, res) => { res.sendStatus(501); }) -r.post('/:id/cancel', 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); @@ -45,7 +47,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, requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { try { const eventID = Number(req.params.id); setEventCancelled(eventID, false); @@ -57,7 +59,7 @@ r.post('/:id/uncancel', async (req: Request, res: Response) => { }) -r.post('/:id/attendance', 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); @@ -85,7 +87,7 @@ r.get('/:id', async (req: Request, res: Response) => { //post a new calendar event -r.post('/', 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; @@ -100,7 +102,7 @@ r.post('/', async (req: Request, res: Response) => { } }) -r.put('/', 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 046b9bb..35bf916 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -1,10 +1,17 @@ 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 "@app/shared/types/member"; 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 { const courses = await getAllCourses(); diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index cbcc30b..6c984b1 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, requireRole } from '../middleware/auth'; + +router.use(requireLogin); //member posts LOA router.post("/", async (req: Request, res: Response) => { @@ -23,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(); @@ -63,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) @@ -101,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); @@ -113,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.ts b/api/src/routes/members.ts index 513351c..f0f06db 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -3,19 +3,16 @@ const router = express.Router(); import { Request, Response } from 'express'; import pool from '../db'; +import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; import { getUserActiveLOA } from '../services/loaService'; import { getMemberSettings, getMembersFull, getMembersLite, getUserData, setUserSettings } from '../services/memberService'; import { getUserRoles } from '../services/rolesService'; -import { memberSettings } from '@app/shared/types/member'; +import { memberSettings, MemberState } from '@app/shared/types/member'; -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) => { +router.get('/', [requireMemberState(MemberState.Member)], async (req, res) => { try { const result = await pool.query( `SELECT @@ -123,4 +120,13 @@ 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); +}); + + + + module.exports = router; diff --git a/api/src/routes/ranks.js b/api/src/routes/ranks.ts similarity index 58% rename from api/src/routes/ranks.js rename to api/src/routes/ranks.ts index cb8c4b1..ee16d13 100644 --- a/api/src/routes/ranks.js +++ b/api/src/routes/ranks.ts @@ -1,10 +1,18 @@ -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') + + +r.use(requireLogin) +ur.use(requireLogin) //insert a new latest rank for a user -ur.post('/', async (req, res) => {3 +ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], 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.ts similarity index 80% rename from api/src/routes/roles.js rename to api/src/routes/roles.ts index 2a435f0..e5429a8 100644 --- a/api/src/routes/roles.js +++ b/api/src/routes/roles.ts @@ -2,11 +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 { 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; @@ -20,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); @@ -38,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(); @@ -77,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 }); @@ -99,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; 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) => { diff --git a/api/src/services/memberService.ts b/api/src/services/memberService.ts index cc57ebc..66b5917 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/memberService.ts @@ -14,18 +14,12 @@ 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); } - export async function getMemberSettings(id: number): Promise { const sql = `SELECT * FROM view_member_settings WHERE id = ?`; let out: memberSettings[] = await pool.query(sql, [id]); 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/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(); 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;