diff --git a/.gitea/workflows/cd-deploy.yaml b/.gitea/workflows/cd-deploy.yaml index 98e5593..8ee90f4 100644 --- a/.gitea/workflows/cd-deploy.yaml +++ b/.gitea/workflows/cd-deploy.yaml @@ -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: | diff --git a/api/.env.example b/api/.env.example index 8bfe497..9a1e8da 100644 --- a/api/.env.example +++ b/api/.env.example @@ -21,6 +21,7 @@ CLIENT_URL= # This is whatever URL the client web app is served on CLIENT_DOMAIN= #whatever.com APPLICATION_VERSION= # Should match release tag APPLICATION_ENVIRONMENT= # dev / prod +CONFIG_ID= # configures # Glitchtip GLITCHTIP_DSN= diff --git a/api/src/index.js b/api/src/index.ts similarity index 59% rename from api/src/index.js rename to api/src/index.ts index 00bf5cc..7230a30 100644 --- a/api/src/index.js +++ b/api/src/index.ts @@ -1,11 +1,15 @@ -const dotenv = require('dotenv') +import dotenv = require('dotenv'); dotenv.config(); -const express = require('express') -const cors = require('cors') -const morgan = require('morgan') +import express = require('express'); +import cors = require('cors'); +import morgan = require('morgan'); const app = express() -app.use(morgan('dev')) +app.use(morgan('dev', { + skip: (req) => { + return req.path === '/members/me'; + } +})) app.use(cors({ origin: [process.env.CLIENT_URL], // your SPA origins @@ -19,7 +23,7 @@ app.set('trust proxy', 1); const port = process.env.SERVER_PORT; //glitchtip setup -const sentry = require('@sentry/node'); +import sentry = require('@sentry/node'); if (process.env.DISABLE_GLITCHTIP === "true") { console.log("Glitchtip disabled") } else { @@ -27,14 +31,14 @@ if (process.env.DISABLE_GLITCHTIP === "true") { let release = process.env.APPLICATION_VERSION; let environment = process.env.APPLICATION_ENVIRONMENT; console.log(release, environment) - sentry.init({ dsn: dsn, release: release, environment: environment }); + sentry.init({ dsn: dsn, release: release, environment: environment, integrations: [sentry.captureConsoleIntegration({ levels: ['error'] })] }); console.log("Glitchtip initialized"); } //session setup -const path = require('path') -const session = require('express-session') -const passport = require('passport') +import path = require('path'); +import session = require('express-session'); +import passport = require('passport'); const SQLiteStore = require('connect-sqlite3')(session); app.use(session({ @@ -51,23 +55,21 @@ app.use(session({ app.use(passport.authenticate('session')); // Mount route modules -const applicationsRouter = require('./routes/applications'); -const { memberRanks, ranks } = require('./routes/ranks'); -const members = require('./routes/members'); -const loaHandler = require('./routes/loa') -const { status, memberStatus } = require('./routes/statuses') -const authRouter = require('./routes/auth') -const { roles, memberRoles } = require('./routes/roles'); -const { courseRouter, eventRouter } = require('./routes/course'); -const { calendarRouter } = require('./routes/calendar') -const morgan = require('morgan'); -const { env } = require('process'); +import { applicationRouter } from './routes/applications'; +import { memberRanks, ranks } from './routes/ranks'; +import { memberRouter } from './routes/members'; +import { loaRouter } from './routes/loa'; +import { status, memberStatus } from './routes/statuses'; +import { authRouter } from './routes/auth'; +import { roles, memberRoles } from './routes/roles'; +import { courseRouter, eventRouter } from './routes/course'; +import { calendarRouter } from './routes/calendar'; -app.use('/application', applicationsRouter); +app.use('/application', applicationRouter); app.use('/ranks', ranks); app.use('/memberRanks', memberRanks); -app.use('/members', members); -app.use('/loa', loaHandler); +app.use('/members', memberRouter); +app.use('/loa', loaRouter); app.use('/status', status) app.use('/memberStatus', memberStatus) app.use('/roles', roles) 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 c79514e..3c78aeb 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -3,15 +3,35 @@ const router = express.Router(); import pool from '../db'; import { approveApplication, createApplication, denyApplication, getAllMemberApplications, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService'; -import { MemberState, setUserState } from '../services/memberService'; +import { setUserState } from '../services/memberService'; +import { MemberState } from '@app/shared/types/member'; import { getRankByName, insertMemberRank } from '../services/rankService'; import { ApplicationFull, CommentRow } from "@app/shared/types/application" import { assignUserToStatus } from '../services/statusService'; -import { Request, Response } from 'express'; +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) => { + const output = await fetch(`${process.env.DOC_HOST}/api/pages/714`, { + headers: { + Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`, + } + }) + + if (output.ok) { + const out = await output.json(); + res.status(200).json(out.html); + } else { + console.error("Failed to fetch LOA policy from bookstack"); + res.sendStatus(500); + } +}) + // POST /application -router.post('/', async (req, res) => { +router.post('/', [requireLogin], async (req, res) => { try { const App = req.body?.App || {}; const memberID = req.user.id; @@ -29,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); @@ -53,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; @@ -78,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 { @@ -105,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) @@ -141,8 +149,9 @@ router.get('/:id', async (req: Request, res: Response) => { }); // POST /application/approve/:id -router.post('/approve/:id', async (req, res) => { - const appID = req.params.id; +router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => { + const appID = Number(req.params.id); + const approved_by = req.user.id; try { const app = await getApplicationByID(appID); @@ -153,14 +162,14 @@ router.post('/approve/:id', async (req, res) => { throw new Error("Something went wrong approving the application"); } - console.log(app.member_id); //update user profile await setUserState(app.member_id, MemberState.Member); - let nextRank = await getRankByName('Recruit') - await insertMemberRank(app.member_id, nextRank.id); - //assign user to "pending basic" - await assignUserToStatus(app.member_id, 1); + await pool.query('CALL sp_accept_new_recruit_validation(?, ?, ?, ?)', [Number(process.env.CONFIG_ID), app.member_id, approved_by, approved_by]) + // let nextRank = await getRankByName('Recruit') + // await insertMemberRank(app.member_id, nextRank.id); + // //assign user to "pending basic" + // await assignUserToStatus(app.member_id, 1); res.sendStatus(200); } catch (err) { console.error('Approve failed:', err); @@ -169,7 +178,7 @@ router.post('/approve/:id', async (req, res) => { }); // 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 { @@ -184,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; @@ -198,8 +207,9 @@ router.post('/:id/comment', async (req: Request, res: Response) => { ) VALUES(?, ?, ?);` + try { - const conn = await pool.getConnection(); + var conn = await pool.getConnection(); const result = await conn.query(sql, [appID, user.id, data]) console.log(result) @@ -223,11 +233,13 @@ VALUES(?, ?, ?);` } catch (err) { console.error('Comment failed:', err); res.status(500).json({ error: 'Could not post comment' }); + } finally { + conn.release(); } }); // 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; @@ -243,7 +255,7 @@ router.post('/:id/adminComment', async (req: Request, res: Response) => { VALUES(?, ?, ?, 1);` try { - const conn = await pool.getConnection(); + var conn = await pool.getConnection(); const result = await conn.query(sql, [appID, user.id, data]) console.log(result) @@ -268,6 +280,8 @@ VALUES(?, ?, ?, 1);` } catch (err) { console.error('Comment failed:', err); res.status(500).json({ error: 'Could not post comment' }); + } finally { + conn.release(); } }); @@ -282,4 +296,4 @@ router.post('/restart', async (req: Request, res: Response) => { } }) -module.exports = router; +export const applicationRouter = router; diff --git a/api/src/routes/auth.js b/api/src/routes/auth.ts similarity index 78% rename from api/src/routes/auth.js rename to api/src/routes/auth.ts index bed32af..c078585 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'); @@ -29,8 +34,9 @@ passport.use(new OpenIDConnectStrategy({ console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); console.log('preferred_username:', jwtClaims?.preferred_username); - const con = await pool.getConnection(); try { + var con = await pool.getConnection(); + await con.beginTransaction(); //lookup existing user @@ -66,12 +72,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 +90,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 +110,18 @@ 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 { + var con = await pool.getConnection(); + 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 +131,18 @@ 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 + +export const authRouter = router; \ No newline at end of file diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index ff486cb..0492501 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); @@ -114,5 +116,4 @@ r.put('/', async (req: Request, res: Response) => { } }) - -module.exports.calendarRouter = r; \ No newline at end of file +export const calendarRouter = r; diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 046b9bb..f04ea08 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -1,11 +1,18 @@ 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(); +const cr = Router(); +const er = Router(); -courseRouter.get('/', async (req, res) => { +cr.use(requireLogin) +er.use(requireLogin) +cr.use(requireMemberState(MemberState.Member)) +er.use(requireMemberState(MemberState.Member)) + +cr.get('/', async (req, res) => { try { const courses = await getAllCourses(); res.status(200).json(courses); @@ -15,7 +22,7 @@ courseRouter.get('/', async (req, res) => { } }) -courseRouter.get('/roles', async (req, res) => { +cr.get('/roles', async (req, res) => { try { const roles = await getCourseEventRoles(); res.status(200).json(roles); @@ -25,7 +32,7 @@ courseRouter.get('/roles', async (req, res) => { } }) -eventRouter.get('/', async (req: Request, res: Response) => { +er.get('/', async (req: Request, res: Response) => { const allowedSorts = new Map([ ["ascending", "ASC"], ["descending", "DESC"] @@ -50,7 +57,7 @@ eventRouter.get('/', async (req: Request, res: Response) => { } }); -eventRouter.get('/:id', async (req: Request, res: Response) => { +er.get('/:id', async (req: Request, res: Response) => { try { let out = await getCourseEventDetails(Number(req.params.id)); res.status(200).json(out); @@ -60,7 +67,7 @@ eventRouter.get('/:id', async (req: Request, res: Response) => { } }); -eventRouter.get('/attendees/:id', async (req: Request, res: Response) => { +er.get('/attendees/:id', async (req: Request, res: Response) => { try { const attendees: CourseAttendee[] = await getCourseEventAttendees(Number(req.params.id)); res.status(200).json(attendees); @@ -70,7 +77,7 @@ eventRouter.get('/attendees/:id', async (req: Request, res: Response) => { } }) -eventRouter.post('/', async (req: Request, res: Response) => { +er.post('/', async (req: Request, res: Response) => { const posterID: number = req.user.id; try { console.log(); @@ -85,5 +92,5 @@ eventRouter.post('/', async (req: Request, res: Response) => { } }) -module.exports.courseRouter = courseRouter; -module.exports.eventRouter = eventRouter; \ No newline at end of file +export const courseRouter = cr; +export const eventRouter = er; diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index cbcc30b..f2de3b1 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) { @@ -145,4 +148,4 @@ router.get('/policy', async (req: Request, res: Response) => { } }) -module.exports = router; +export const loaRouter = router; \ No newline at end of file diff --git a/api/src/routes/members.js b/api/src/routes/members.ts similarity index 53% rename from api/src/routes/members.js rename to api/src/routes/members.ts index 3196569..26e3fa8 100644 --- a/api/src/routes/members.js +++ b/api/src/routes/members.ts @@ -1,19 +1,16 @@ const express = require('express'); 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 { getUserData } from '../services/memberService'; +import { getMemberSettings, getMembersFull, getMembersLite, getUserData, setUserSettings } from '../services/memberService'; import { getUserRoles } from '../services/rolesService'; - -router.use((req, res, next) => { - console.log(req.user); - console.log('Time:', Date.now()) - next() -}) +import { memberSettings, MemberState } from '@app/shared/types/member'; //get all users -router.get('/', async (req, res) => { +router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => { try { const result = await pool.query( `SELECT @@ -27,7 +24,7 @@ router.get('/', async (req, res) => { AND UTC_TIMESTAMP() BETWEEN l.start_date AND l.end_date ) THEN 1 ELSE 0 END AS on_loa - FROM view_member_rank_status_all v;`); + FROM view_member_rank_unit_status_latest v;`); return res.status(200).json(result); } catch (err) { console.error('Error fetching users:', err); @@ -35,7 +32,7 @@ router.get('/', async (req, res) => { } }); -router.get('/me', async (req, res) => { +router.get('/me', [requireLogin], async (req, res) => { if (req.user === undefined) return res.sendStatus(401) @@ -60,10 +57,57 @@ router.get('/me', async (req, res) => { } }) -router.get('/:id', async (req, res) => { +router.get('/settings', [requireLogin], async (req: Request, res: Response) => { + try { + let user = req.user.id; + console.log(user); + let output = await getMemberSettings(user); + res.status(200).json(output); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.put('/settings', [requireLogin], async (req: Request, res: Response) => { + try { + let user = req.user.id; + let settings: memberSettings = req.body; + console.log(settings) + await setUserSettings(user, settings); + res.sendStatus(200); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.post('/lite/bulk', async (req: Request, res: Response) => { + try { + let ids = req.body.ids; + let out = await getMembersLite(ids); + res.status(200).json(out); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.post('/full/bulk', async (req: Request, res: Response) => { + try { + let ids = req.body.ids; + let out = await getMembersFull(ids); + res.status(200).json(out); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.get('/:id', [requireLogin], async (req, res) => { try { const userId = req.params.id; - const result = await pool.query('SELECT * FROM view_member_rank_status_all WHERE id = $1;', [userId]); + const result = await pool.query('SELECT * FROM view_member_rank_unit_status_latest WHERE id = $1;', [userId]); if (result.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } @@ -77,10 +121,8 @@ 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); }); - - -module.exports = router; +export const memberRouter = router; diff --git a/api/src/routes/ranks.js b/api/src/routes/ranks.ts similarity index 56% rename from api/src/routes/ranks.js rename to api/src/routes/ranks.ts index cb8c4b1..5eff733 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); @@ -27,5 +35,5 @@ r.get('/', async (req, res) => { } }); -module.exports.ranks = r; -module.exports.memberRanks = ur; \ No newline at end of file +export const ranks = r; +export const memberRanks = ur; \ No newline at end of file diff --git a/api/src/routes/roles.js b/api/src/routes/roles.ts similarity index 76% rename from api/src/routes/roles.js rename to api/src/routes/roles.ts index f1857f6..4b692af 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,9 +43,9 @@ 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(); + var con = await pool.getConnection(); // Get all roles const roles = await con.query('SELECT * FROM roles;'); @@ -49,7 +54,7 @@ r.get('/', async (req, res) => { const membersRoles = await con.query(` SELECT mr.role_id, v.* FROM members_roles mr - JOIN view_member_rank_status_all v ON mr.member_id = v.member_id + JOIN view_member_rank_unit_status_latest v ON mr.member_id = v.member_id `); @@ -68,16 +73,17 @@ r.get('/', async (req, res) => { members: roleIdToMembers[role.id] || [] })); - con.release(); res.json(result); } catch (err) { console.error(err); res.status(500).json({ error: 'Internal server error' }); + } finally { + con.release(); } }); //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 +105,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; @@ -112,5 +118,5 @@ r.delete('/:id', async (req, res) => { } }) -module.exports.roles = r; -module.exports.memberRoles = ur; \ No newline at end of file +export const roles = r; +export const memberRoles = ur; diff --git a/api/src/routes/statuses.js b/api/src/routes/statuses.ts similarity index 70% rename from api/src/routes/statuses.js rename to api/src/routes/statuses.ts index 8e9d48e..8457800 100644 --- a/api/src/routes/statuses.js +++ b/api/src/routes/statuses.ts @@ -1,11 +1,15 @@ -const express = require('express'); -const status = express.Router(); -const memberStatus = express.Router(); +import express = require('express'); +const statusR = express.Router(); +const memberStatusR = express.Router(); import pool from '../db'; +import { requireLogin } from '../middleware/auth'; + +statusR.use(requireLogin); +memberStatusR.use(requireLogin); //insert a new latest rank for a user -memberStatus.post('/', async (req, res) => { +memberStatusR.post('/', async (req, res) => { // try { // const App = req.body?.App || {}; @@ -30,7 +34,7 @@ memberStatus.post('/', async (req, res) => { }); //get all statuses -status.get('/', async (req, res) => { +statusR.get('/', async (req, res) => { try { const result = await pool.query('SELECT * FROM statuses;'); res.json(result); @@ -40,7 +44,8 @@ status.get('/', async (req, res) => { } }); -module.exports.status = status; -module.exports.memberStatus = memberStatus; +export const status = statusR; +export const memberStatus = memberStatusR; + // TODO, implement get all ranks route with SQL stirng SELECT id, name, short_name, category, sort_id FROM ranks; \ No newline at end of file diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 1cadc31..f365da6 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -79,9 +79,9 @@ export async function getCourseEventDetails(id: number): Promise { - console.log(event); - const con = await pool.getConnection(); try { + var con = await pool.getConnection(); + await con.beginTransaction(); const res = await con.query("INSERT INTO course_events (course_id, event_date, remarks, created_by) VALUES (?, ?, ?, ?);", [event.course_id, toDateTime(event.event_date), event.remarks, event.created_by]); var eventID: number = res.insertId; @@ -98,12 +98,12 @@ export async function insertCourseEvent(event: CourseEventDetails): 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]); + + if (out.length != 1) + throw new Error("Could not get user settings"); + + return out[0]; +} + +export async function setUserSettings(id: number, settings: memberSettings) { + const sql = `UPDATE view_member_settings SET + displayName = ? + WHERE id = ?;`; + let result = await pool.query(sql, [settings.displayName, id]) + console.log(result); +} + +export async function getMembersLite(ids: number[]): Promise { + const sql = `SELECT m.member_id AS id, + m.member_name AS username, + m.displayName, + u.color + FROM view_member_rank_unit_status_latest m + LEFT JOIN units u ON u.name = m.unit + WHERE member_id IN (?);`; + const res: MemberLight[] = await pool.query(sql, [ids]); + return res; +} + +export async function getMembersFull(ids: number[]): Promise { + const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id IN (?);`; + const res: Member[] = await pool.query(sql, [ids]); + return res; +} \ No newline at end of file diff --git a/api/src/services/rankService.ts b/api/src/services/rankService.ts index f29a8b3..ada4ea0 100644 --- a/api/src/services/rankService.ts +++ b/api/src/services/rankService.ts @@ -21,8 +21,8 @@ export async function insertMemberRank(member_id: number, rank_id: number): Prom export async function insertMemberRank(member_id: number, rank_id: number, date?: Date): Promise { const sql = date - ? `INSERT INTO members_ranks (member_id, rank_id, event_date) VALUES (?, ?, ?);` - : `INSERT INTO members_ranks (member_id, rank_id, event_date) VALUES (?, ?, NOW());`; + ? `INSERT INTO members_ranks (member_id, rank_id, start_date) VALUES (?, ?, ?);` + : `INSERT INTO members_ranks (member_id, rank_id, start_date) VALUES (?, ?, NOW());`; const params = date ? [member_id, rank_id, date] diff --git a/api/src/services/statusService.ts b/api/src/services/statusService.ts index 7a62f3a..8b8fb28 100644 --- a/api/src/services/statusService.ts +++ b/api/src/services/statusService.ts @@ -1,6 +1,6 @@ import pool from "../db" export async function assignUserToStatus(userID: number, statusID: number) { - const sql = `INSERT INTO members_statuses (member_id, status_id, event_date) VALUES (?, ?, NOW())` + const sql = `INSERT INTO members_statuses (member_id, status_id, start_date) VALUES (?, ?, NOW())` await pool.execute(sql, [userID, statusID]); } diff --git a/shared/types/member.ts b/shared/types/member.ts new file mode 100644 index 0000000..20e0469 --- /dev/null +++ b/shared/types/member.ts @@ -0,0 +1,31 @@ +export interface memberSettings { + displayName: string; +} + +export enum MemberState { + Guest = "guest", + Applicant = "applicant", + Member = "member", + Retired = "retired", + Banned = "banned", + Denied = "denied" +} + +export type Member = { + member_id: number; + member_name: string; + rank: string | null; + rank_date: string | null; + unit: string | null; + unit_date: string | null; + status: string | null; + status_date: string | null; + loa_until?: Date; +}; + +export interface MemberLight { + id: number + displayName: string + username: string + color: string +} \ No newline at end of file diff --git a/ui/src/api/application.ts b/ui/src/api/application.ts index 8f60f1e..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() @@ -89,7 +91,7 @@ export async function getMyApplication(id: number): Promise { } export async function approveApplication(id: Number) { - const res = await fetch(`${addr}/application/approve/${id}`, { method: 'POST' }) + const res = await fetch(`${addr}/application/approve/${id}`, { method: 'POST', credentials: 'include' }) if (!res.ok) { console.error("Something went wrong approving the application") @@ -97,7 +99,7 @@ export async function approveApplication(id: Number) { } export async function denyApplication(id: Number) { - const res = await fetch(`${addr}/application/deny/${id}`, { method: 'POST' }) + const res = await fetch(`${addr}/application/deny/${id}`, { method: 'POST', credentials: 'include' }) if (!res.ok) { console.error("Something went wrong denying the application") @@ -113,4 +115,20 @@ export async function restartApplication() { if (!res.ok) { console.error("Something went wrong restarting your application") } +} + +export async function getCoC(): Promise { + const res = await fetch(`${addr}/application/coc`, { + method: "GET", + credentials: 'include', + }); + if (res.ok) { + const out = res.json(); + if (!out) { + return null; + } + return out; + } else { + return null; + } } \ No newline at end of file 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/member.ts b/ui/src/api/member.ts index b97e7ac..9fe2935 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,12 +1,4 @@ -export type Member = { - member_id: number; - member_name: string; - rank: string | null; - rank_date: string | null; - status: string | null; - status_date: string | null; - on_loa: boolean | null; -}; +import { memberSettings, Member, MemberLight } from "@shared/types/member"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -19,4 +11,66 @@ export async function getMembers(): Promise { throw new Error("Failed to fetch members"); } return response.json(); +} + +export async function getMemberSettings(): Promise { + const response = await fetch(`${addr}/members/settings`, { + credentials: 'include' + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return response.json(); +} + +export async function setMemberSettings(settings: memberSettings) { + const response = await fetch(`${addr}/members/settings`, { + credentials: 'include', + method: 'PUT', + headers: { + 'Content-Type': 'Application/json', + }, + body: JSON.stringify(settings) + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return; +} + +export async function getLightMembers(ids: number[]): Promise { + + if (ids.length === 0) return []; + + const response = await fetch(`${addr}/members/lite/bulk`, { + credentials: 'include', + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ids }) + }); + + if (!response.ok) { + throw new Error("Failed to fetch light members"); + } + return response.json(); +} + +export async function getFullMembers(ids: number[]): Promise { + + if (ids.length === 0) return []; + + const response = await fetch(`${addr}/members/full/bulk`, { + credentials: 'include', + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ids }) + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return response.json(); } \ No newline at end of file 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; diff --git a/ui/src/assets/base.css b/ui/src/assets/base.css index b29a3bb..f06ddf1 100644 --- a/ui/src/assets/base.css +++ b/ui/src/assets/base.css @@ -168,7 +168,7 @@ } /* Root container */ -.ListRendererV2-container { +.bookstack-container { font-family: var(--font-sans, system-ui), sans-serif; color: var(--foreground); line-height: 1.45; @@ -178,56 +178,53 @@ } /* Headers */ -.ListRendererV2-container h4 { +.bookstack-container h4 { margin: 0.9rem 0 0.4rem 0; font-weight: 600; line-height: 1.35; font-size: 1.05rem; color: var(--foreground); - /* PURE WHITE */ } -.ListRendererV2-container h5 { +.bookstack-container h5 { margin: 0.9rem 0 0.4rem 0; font-weight: 600; line-height: 1.35; font-size: 0.95rem; color: var(--foreground); - /* Still white (change to muted if desired) */ } /* Lists */ -.ListRendererV2-container ul { +.bookstack-container ul { list-style-type: disc; margin-left: 1.1rem; margin-bottom: 0.6rem; padding-left: 0.6rem; color: var(--muted-foreground); - /* dim text */ } /* Nested lists */ -.ListRendererV2-container ul ul { +.bookstack-container ul ul { list-style-type: circle; margin-left: 0.9rem; } /* List items */ -.ListRendererV2-container li { +.bookstack-container li { margin: 0.15rem 0; padding-left: 0.1rem; color: var(--muted-foreground); } /* Bullet color */ -.ListRendererV2-container li::marker { +.bookstack-container li::marker { color: var(--muted-foreground); } /* Inline elements */ -.ListRendererV2-container li p, -.ListRendererV2-container li span, -.ListRendererV2-container p { +.bookstack-container li p, +.bookstack-container li span, +.bookstack-container p { display: inline; margin: 0; padding: 0; @@ -235,6 +232,45 @@ } /* Top-level spacing */ -.ListRendererV2-container>ul>li { +.bookstack-container>ul>li { margin-top: 0.3rem; +} + +/* links */ +.bookstack-container a { + color: var(--color-primary); + margin-top: 0.3rem; +} + +.bookstack-container a:hover { + text-decoration: underline; +} + +/* Scrollbar stuff */ +/* Firefox */ +.scrollbar-themed { + scrollbar-width: thin; + scrollbar-color: #555 #1f1f1f; + padding-right: 6px; +} + +/* Chrome, Edge, Safari */ +.scrollbar-themed::-webkit-scrollbar { + width: 10px; + /* slightly wider to allow padding look */ +} + +.scrollbar-themed::-webkit-scrollbar-track { + background: #1f1f1f; + margin-left: 6px; + /* ❗ adds space between content + scrollbar */ +} + +.scrollbar-themed::-webkit-scrollbar-thumb { + background: #555; + border-radius: 9999px; +} + +.scrollbar-themed::-webkit-scrollbar-thumb:hover { + background: #777; } \ No newline at end of file diff --git a/ui/src/components/Navigation/Navbar.vue b/ui/src/components/Navigation/Navbar.vue index 0d777d8..a13d3f7 100644 --- a/ui/src/components/Navigation/Navbar.vue +++ b/ui/src/components/Navigation/Navbar.vue @@ -19,6 +19,8 @@ import NavigationMenuContent from '../ui/navigation-menu/NavigationMenuContent.v import { navigationMenuTriggerStyle } from '../ui/navigation-menu/' import { useAuth } from '@/composables/useAuth'; import { ArrowUpRight, CircleArrowOutUpRight } from 'lucide-vue-next'; +import DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue'; +import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue'; const userStore = useUserStore(); const auth = useAuth(); @@ -180,10 +182,12 @@ function blurAfter() {

{{ userStore.user.name }}

- + My Profile + My Application Application History + Logout diff --git a/ui/src/components/application/ApplicationChat.vue b/ui/src/components/application/ApplicationChat.vue index 2501d50..fb8f8bf 100644 --- a/ui/src/components/application/ApplicationChat.vue +++ b/ui/src/components/application/ApplicationChat.vue @@ -15,10 +15,14 @@ import { useAuth } from '@/composables/useAuth' import { CommentRow } from '@shared/types/application' import { Dot } from 'lucide-vue-next' import { ref } from 'vue' +import MemberCard from '../members/MemberCard.vue' -const props = defineProps<{ +const props = withDefaults(defineProps<{ messages: CommentRow[] -}>() + adminMode?: boolean +}>(), { + adminMode: false, +}) const emit = defineEmits<{ (e: 'post', text: string): void @@ -59,7 +63,7 @@ function onSubmit(values: { text: string }, { resetForm }: { resetForm: () => vo
- +
@@ -71,7 +75,7 @@ function onSubmit(values: { text: string }, { resetForm }: { resetForm: () => vo
-

{{ message.poster_name }}

+

Internal

diff --git a/ui/src/components/application/ApplicationForm.vue b/ui/src/components/application/ApplicationForm.vue index eb1a85f..5ad43b9 100644 --- a/ui/src/components/application/ApplicationForm.vue +++ b/ui/src/components/application/ApplicationForm.vue @@ -13,10 +13,18 @@ import Input from '@/components/ui/input/Input.vue'; import Textarea from '@/components/ui/textarea/Textarea.vue'; import { toTypedSchema } from '@vee-validate/zod'; import { Form } from 'vee-validate'; -import { onMounted, ref } from 'vue'; +import { nextTick, onMounted, ref, watch } from 'vue'; import * as z from 'zod'; import DateInput from '../form/DateInput.vue'; import { ApplicationData } from '@shared/types/application'; +import Dialog from '../ui/dialog/Dialog.vue'; +import DialogTrigger from '../ui/dialog/DialogTrigger.vue'; +import DialogContent from '../ui/dialog/DialogContent.vue'; +import DialogHeader from '../ui/dialog/DialogHeader.vue'; +import DialogTitle from '../ui/dialog/DialogTitle.vue'; +import DialogDescription from '../ui/dialog/DialogDescription.vue'; +import { getCoC } from '@/api/application'; +import { startBrowserTracingPageLoadSpan } from '@sentry/vue'; const regexA = /^https?:\/\/steamcommunity\.com\/id\/[A-Za-z0-9_]+\/?$/; const regexB = /^https?:\/\/steamcommunity\.com\/profiles\/\d+\/?$/; @@ -61,7 +69,7 @@ async function onSubmit(val: any) { emit('submit', val); } -onMounted(() => { +onMounted(async () => { if (props.data !== null) { const parsed = typeof props.data === "string" ? JSON.parse(props.data) @@ -71,8 +79,35 @@ onMounted(() => { } else { initialValues.value = { ...fallbackInitials }; } + + // CoCbox.value.innerHTML = await getCoC() + CoCString.value = await getCoC(); }) +const showCoC = ref(false); +const CoCbox = ref(); +const CoCString = ref(); + +async function onDialogToggle(state: boolean) { + showCoC.value = state; +} + +function enforceExternalLinks() { + if (!CoCbox.value) return; + + const links = CoCbox.value.querySelectorAll("a"); + links.forEach(a => { + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "noopener noreferrer"); + }); +} + +watch(() => showCoC.value, async () => { + if (showCoC) { + await nextTick(); // wait for v-html to update + enforceExternalLinks(); + } +}); @@ -273,7 +308,8 @@ onMounted(() => {
- By checking this box, you accept the .
@@ -284,7 +320,19 @@ onMounted(() => {
- +
+ + + + + Community Code of Conduct + +
+
+
+
+
+ \ No newline at end of file diff --git a/ui/src/components/calendar/ViewCalendarEvent.vue b/ui/src/components/calendar/ViewCalendarEvent.vue index c99e0dc..22217cc 100644 --- a/ui/src/components/calendar/ViewCalendarEvent.vue +++ b/ui/src/components/calendar/ViewCalendarEvent.vue @@ -12,6 +12,7 @@ import DropdownMenuTrigger from '../ui/dropdown-menu/DropdownMenuTrigger.vue'; import DropdownMenuContent from '../ui/dropdown-menu/DropdownMenuContent.vue'; import DropdownMenuItem from '../ui/dropdown-menu/DropdownMenuItem.vue'; import { Calendar } from 'lucide-vue-next'; +import MemberCard from '../members/MemberCard.vue'; const route = useRoute(); @@ -239,7 +240,7 @@ defineExpose({ forceReload }) {{ activeEvent.location || "Unknown" }}
- {{ activeEvent.creator_name || "Unknown User" }} +
@@ -276,7 +277,9 @@ defineExpose({ forceReload })
-

{{ person.member_name }}

+
+ +

{{ displayStatus(person.status) }}

diff --git a/ui/src/components/loa/loaForm.vue b/ui/src/components/loa/loaForm.vue index 3ca59a1..a7362b8 100644 --- a/ui/src/components/loa/loaForm.vue +++ b/ui/src/components/loa/loaForm.vue @@ -132,7 +132,7 @@ const maxEndDate = computed(() => {

LOA Policy

-
+
diff --git a/ui/src/components/loa/loaList.vue b/ui/src/components/loa/loaList.vue index 2151d1c..386f392 100644 --- a/ui/src/components/loa/loaList.vue +++ b/ui/src/components/loa/loaList.vue @@ -32,6 +32,7 @@ import { getLocalTimeZone, } from "@internationalized/date" import { el } from "@fullcalendar/core/internal-common"; +import MemberCard from "../members/MemberCard.vue"; const props = defineProps<{ adminMode?: boolean @@ -146,7 +147,7 @@ async function commitExtend() { - {{ post.name }} + {{ post.type_name }} {{ formatDate(post.start_date) }} diff --git a/ui/src/components/members/MemberCard.vue b/ui/src/components/members/MemberCard.vue new file mode 100644 index 0000000..0ba3352 --- /dev/null +++ b/ui/src/components/members/MemberCard.vue @@ -0,0 +1,153 @@ + + + diff --git a/ui/src/components/ui/spinner/Spinner.vue b/ui/src/components/ui/spinner/Spinner.vue new file mode 100644 index 0000000..077c490 --- /dev/null +++ b/ui/src/components/ui/spinner/Spinner.vue @@ -0,0 +1,16 @@ + + + diff --git a/ui/src/components/ui/spinner/index.js b/ui/src/components/ui/spinner/index.js new file mode 100644 index 0000000..8a95e7a --- /dev/null +++ b/ui/src/components/ui/spinner/index.js @@ -0,0 +1 @@ +export { default as Spinner } from "./Spinner.vue"; diff --git a/ui/src/pages/Application.vue b/ui/src/pages/Application.vue index 0c30c0e..6092ab1 100644 --- a/ui/src/pages/Application.vue +++ b/ui/src/pages/Application.vue @@ -164,7 +164,7 @@ async function handleDeny(id) {

Discussion

- +
diff --git a/ui/src/pages/ManageApplications.vue b/ui/src/pages/ManageApplications.vue index a19316b..bc5a347 100644 --- a/ui/src/pages/ManageApplications.vue +++ b/ui/src/pages/ManageApplications.vue @@ -1,5 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index 46ad7ea..1985cf8 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -21,6 +21,7 @@ import SelectValue from '@/components/ui/select/SelectValue.vue'; import SelectContent from '@/components/ui/select/SelectContent.vue'; import SelectItem from '@/components/ui/select/SelectItem.vue'; import Input from '@/components/ui/input/Input.vue'; +import MemberCard from '@/components/members/MemberCard.vue'; enum sidePanelState { view, create, closed }; @@ -152,9 +153,13 @@ onMounted(async () => { {{ report.course_name.length > 30 ? report.course_shortname : report.course_name }} {{ report.date.split('T')[0] }} - {{ report.created_by_name === null ? "Unknown User" : + + + Unknown User + + @@ -172,11 +177,14 @@ onMounted(async () => {

{{ focusedTrainingReport.course_name }}

-
+

{{ focusedTrainingReport.event_date.split('T')[0] }}

-

Created by {{ focusedTrainingReport.created_by_name === null ? "Unknown User" : +

Created by: + +

{{ focusedTrainingReport.created_by_name === null ? "Unknown User" : focusedTrainingReport.created_by_name - }} + }}

@@ -191,7 +199,11 @@ onMounted(async () => {
-

{{ person.attendee_name }}

+
+ +

{{ person.attendee_name }}

+

{{ person.role.name }}

@@ -213,7 +225,11 @@ onMounted(async () => {

-

{{ person.attendee_name }}

+
+ +

{{ person.attendee_name }}

+
@@ -242,7 +258,11 @@ onMounted(async () => {
-

{{ person.attendee_name }}

+
+ +

{{ person.attendee_name }}

+