5 Commits

Author SHA1 Message Date
b91ecacb60 implemented role and state based authorization 2025-12-13 17:01:50 -05:00
7c4e8d7db8 Implemented login requirement for most of the API 2025-12-13 14:25:39 -05:00
2ea355d9d8 fix tagging on release v2
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m26s
Continuous Deployment / Update Deployment (push) Successful in 2m19s
always check your quotes
2025-12-12 19:26:05 -06:00
4d19f26f01 fix tagging in workflow
Some checks failed
Continuous Integration / Update Development (push) Successful in 2m31s
Continuous Deployment / Update Deployment (push) Failing after 1m42s
apparently git pull doesn't properly fetch new tags on upstream, so call that fetch first
2025-12-12 16:16:48 -06:00
445c15b797 Merge pull request 'database-view-updates' (#67) from database-view-updates into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m17s
Continuous Deployment / Update Deployment (push) Successful in 2m10s
Reviewed-on: #67
2025-12-12 10:27:18 -06:00
14 changed files with 150 additions and 75 deletions

View File

@@ -48,12 +48,12 @@ jobs:
cd /var/www/html/milsim-site-v4
version=`git log -1 --format=%H`
echo "Current Revision: $version"
echo "Updating to: ${{ github.sha }}
echo "Updating to: ${{ github.sha }}"
sudo -u nginx git reset --hard
sudo -u nginx git pull origin main
sudo -u nginx git fetch --tags
sudo -u nginx git pull origin main
new_version=`git log -1 --format=%H`
echo "Sucessfully updated to: $new_version
echo "Successfully updated to: $new_version"
- name: Update Shared Dependencies and Fix Permissions
run: |

View File

@@ -0,0 +1,49 @@
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)
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 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);
}
};
}

View File

@@ -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, requireRole } 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, requireRole("Recruiter")], 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,22 +124,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)
@@ -159,7 +148,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;
@@ -188,7 +177,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 {
@@ -203,7 +192,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 +235,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;

View File

@@ -6,7 +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');
@@ -21,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 {
@@ -66,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;
@@ -90,7 +88,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 +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 {
@@ -128,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;

View File

@@ -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);

View File

@@ -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();

View File

@@ -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) {

View File

@@ -2,18 +2,15 @@ const express = require('express');
const router = express.Router();
import pool from '../db';
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((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

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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) => {

View File

@@ -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<MemberState> {
let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]);
console.log('hi')
return (out[0].state as MemberState);
}

View File

@@ -59,7 +59,9 @@ export async function postAdminChatMessage(message: any, post_id: number) {
}
export async function getAllApplications(): Promise<ApplicationFull> {
const res = await fetch(`${addr}/application/all`)
const res = await fetch(`${addr}/application/all`, {
credentials: 'include',
})
if (res.ok) {
return res.json()

View File

@@ -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<CourseEventSummary[]> {
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<CourseEventSummary[]>;
@@ -15,7 +17,9 @@ export async function getTrainingReports(sortMode: string, search: string): Prom
}
export async function getTrainingReport(id: number): Promise<CourseEventDetails> {
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<CourseEventDetails>;
@@ -26,7 +30,9 @@ export async function getTrainingReport(id: number): Promise<CourseEventDetails>
}
export async function getAllTrainings(): Promise<Course[]> {
const res = await fetch(`${addr}/course`);
const res = await fetch(`${addr}/course`, {
credentials: 'include',
});
if (res.ok) {
return await res.json() as Promise<Course[]>;
@@ -37,7 +43,9 @@ export async function getAllTrainings(): Promise<Course[]> {
}
export async function getAllAttendeeRoles(): Promise<CourseAttendeeRole[]> {
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<CourseAttendeeRole[]>;