Merge remote-tracking branch 'Origin/main' into devcontainers
This commit is contained in:
@@ -21,7 +21,13 @@ 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
|
||||
CONFIG_ID= # config version
|
||||
|
||||
# webhooks/integrations
|
||||
DISCORD_APPLICATIONS_WEBHOOK=
|
||||
|
||||
# Logger
|
||||
LOG_DEPTH= # normal / verbose / profiling
|
||||
|
||||
# Glitchtip
|
||||
GLITCHTIP_DSN=
|
||||
|
||||
19
api/package-lock.json
generated
19
api/package-lock.json
generated
@@ -11,7 +11,6 @@
|
||||
"dependencies": {
|
||||
"@sentry/node": "^10.27.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"chalk": "^5.6.2",
|
||||
"connect-sqlite3": "^0.9.16",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "16.6.1",
|
||||
@@ -1323,18 +1322,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -3286,9 +3273,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"dependencies": {
|
||||
"@sentry/node": "^10.27.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"chalk": "^5.6.2",
|
||||
"connect-sqlite3": "^0.9.16",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "16.6.1",
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
// const mariadb = require('mariadb')
|
||||
import * as mariadb from 'mariadb';
|
||||
// const dotenv = require('dotenv')
|
||||
// import path = require('path');
|
||||
// console.log('NODE_ENV =', process.env.NODE_ENV);
|
||||
|
||||
// const envFile = process.env.NODE_ENV === 'development' ? '.env.development' : '.env';
|
||||
// dotenv.config({ path: path.resolve(process.cwd(), envFile) });
|
||||
// console.log(`Loaded environment from ${envFile}`);
|
||||
|
||||
// console.log(process.env.DB_HOST)
|
||||
|
||||
const pool = mariadb.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
import dotenv = require('dotenv');
|
||||
dotenv.config();
|
||||
dotenv.config({ quiet: true });
|
||||
|
||||
import express = require('express');
|
||||
import cors = require('cors');
|
||||
import morgan = require('morgan');
|
||||
import { logger, LogHeader, LogPayload } from './services/logging/logger';
|
||||
|
||||
const app = express()
|
||||
import chalk from 'chalk';
|
||||
|
||||
app.use(morgan((tokens: morgan.TokenIndexer, req: express.Request, res: express.Response) => {
|
||||
const status = Number(tokens.status(req, res));
|
||||
|
||||
// Colorize status code
|
||||
const statusColor = status >= 500 ? chalk.red
|
||||
: status >= 400 ? chalk.yellow
|
||||
: status >= 300 ? chalk.cyan
|
||||
: chalk.green;
|
||||
const head: LogHeader = {
|
||||
type: 'http',
|
||||
level: 'info',
|
||||
depth: 'normal',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
return [
|
||||
chalk.gray(`[${new Date().toISOString()}]`),
|
||||
chalk.blue.bold(tokens.method(req, res)),
|
||||
tokens.url(req, res),
|
||||
statusColor(status),
|
||||
chalk.magenta(tokens['response-time'](req, res) + ' ms'),
|
||||
chalk.yellow(`- User: ${req.user?.name ? `${req.user.name} (${req.user.id})` : 'Unauthenticated'}`),
|
||||
].join(' ');
|
||||
const payload: LogPayload = {
|
||||
message: 'HTTP request completed',
|
||||
data: {
|
||||
method: tokens.method(req, res),
|
||||
path: tokens.url(req, res),
|
||||
status: Number(tokens.status(req, res)),
|
||||
response_time_ms: Number(tokens['response-time'](req, res)),
|
||||
user_id: req.user?.id,
|
||||
user_name: req.user?.name,
|
||||
user_agent: req.headers['user-agent'],
|
||||
},
|
||||
}
|
||||
|
||||
logger.log(head.level, head.type, payload.message, payload.data, head.depth)
|
||||
return '';
|
||||
}, {
|
||||
skip: (req: express.Request) => {
|
||||
return req.originalUrl === '/members/me';
|
||||
return req.originalUrl === '/members/me' || req.originalUrl === '/ping';
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -43,14 +52,13 @@ const port = process.env.SERVER_PORT;
|
||||
//glitchtip setup
|
||||
import sentry = require('@sentry/node');
|
||||
if (process.env.DISABLE_GLITCHTIP === "true") {
|
||||
console.log("Glitchtip disabled")
|
||||
logger.info('app', 'Glitchtip disabled', null, 'normal')
|
||||
} else {
|
||||
let dsn = process.env.GLITCHTIP_DSN;
|
||||
let release = process.env.APPLICATION_VERSION;
|
||||
let environment = process.env.APPLICATION_ENVIRONMENT;
|
||||
console.log(release, environment)
|
||||
sentry.init({ dsn: dsn, release: release, environment: environment, integrations: [sentry.captureConsoleIntegration({ levels: ['error'] })] });
|
||||
console.log("Glitchtip initialized");
|
||||
logger.info('app', 'Glitchtip initialized', null, 'normal')
|
||||
}
|
||||
|
||||
//session setup
|
||||
@@ -75,6 +83,11 @@ const sessionOptions: session.SessionOptions = {
|
||||
cookie: cookieOptions
|
||||
}
|
||||
|
||||
import { initializeDiscordIntegrations } from './services/integrations/discord';
|
||||
|
||||
//event bus setup
|
||||
initializeDiscordIntegrations();
|
||||
|
||||
app.use(session(sessionOptions));
|
||||
app.use(passport.authenticate('session'));
|
||||
|
||||
@@ -110,5 +123,5 @@ app.get('/ping', (req, res) => {
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening on port ${port} `)
|
||||
logger.info('app', `Example app listening on port ${port} `)
|
||||
})
|
||||
|
||||
@@ -2,59 +2,97 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
import pool from '../db';
|
||||
import { approveApplication, createApplication, denyApplication, getAllMemberApplications, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService';
|
||||
import { setUserState } from '../services/memberService';
|
||||
import { approveApplication, createApplication, denyApplication, getAllMemberApplications, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/db/applicationService';
|
||||
import { setUserState } from '../services/db/memberService';
|
||||
import { MemberState } from '@app/shared/types/member';
|
||||
import { getRankByName, insertMemberRank } from '../services/rankService';
|
||||
import { getRankByName, insertMemberRank } from '../services/db/rankService';
|
||||
import { ApplicationFull, CommentRow } from "@app/shared/types/application"
|
||||
import { assignUserToStatus } from '../services/statusService';
|
||||
import { assignUserToStatus } from '../services/db/statusService';
|
||||
import { Request, response, Response } from 'express';
|
||||
import { getUserRoles } from '../services/rolesService';
|
||||
import { getUserRoles } from '../services/db/rolesService';
|
||||
import { requireLogin, requireRole } from '../middleware/auth';
|
||||
import { logger } from '../services/logging/logger';
|
||||
import { bus } from '../services/events/eventBus';
|
||||
|
||||
//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}`,
|
||||
}
|
||||
})
|
||||
try {
|
||||
const response = 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();
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
logger.error('app', 'Failed to fetch LOA policy from Bookstack', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: text,
|
||||
});
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
const out = await response.json();
|
||||
res.status(200).json(out.html);
|
||||
} else {
|
||||
console.error("Failed to fetch LOA policy from bookstack");
|
||||
|
||||
} catch (error) {
|
||||
logger.error('app', 'Error fetching LOA policy from Bookstack', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
res.sendStatus(500);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// POST /application
|
||||
router.post('/', [requireLogin], async (req, res) => {
|
||||
router.post('/', [requireLogin], async (req: Request, res: Response) => {
|
||||
const memberID = req.user.id;
|
||||
const App = req.body?.App || {};
|
||||
const appVersion = 1;
|
||||
|
||||
try {
|
||||
const App = req.body?.App || {};
|
||||
const memberID = req.user.id;
|
||||
let appID = await createApplication(memberID, appVersion, JSON.stringify(App));
|
||||
|
||||
const appVersion = 1;
|
||||
|
||||
await createApplication(memberID, appVersion, JSON.stringify(App))
|
||||
await setUserState(memberID, MemberState.Applicant);
|
||||
|
||||
res.sendStatus(201);
|
||||
|
||||
bus.emit("application.create", { application: appID, member_name: req.user.name, member_discord_id: req.user.discord_id || null })
|
||||
|
||||
logger.info('app', 'Application Posted', {
|
||||
user: memberID,
|
||||
app: appID
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to create application: \n', err);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to create application',
|
||||
{
|
||||
memberID,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Failed to create application' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// GET /application/all
|
||||
router.get('/all', [requireLogin, requireRole("Recruiter")], async (req, res) => {
|
||||
try {
|
||||
const rows = await getApplicationList();
|
||||
res.status(200).json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get applications',
|
||||
{
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500);
|
||||
}
|
||||
});
|
||||
@@ -68,8 +106,16 @@ router.get('/meList', async (req, res) => {
|
||||
|
||||
return res.status(200).json(application);
|
||||
} catch (error) {
|
||||
console.error('Failed to load applications: \n', error);
|
||||
return res.status(500).json(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get applications for user',
|
||||
{
|
||||
user: userID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
return res.status(500);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -80,8 +126,10 @@ router.get('/me', [requireLogin], async (req, res) => {
|
||||
try {
|
||||
let application = await getMemberApplication(userID);
|
||||
|
||||
if (application === undefined)
|
||||
if (application === undefined) {
|
||||
res.sendStatus(204);
|
||||
return;
|
||||
}
|
||||
|
||||
const comments: CommentRow[] = await getApplicationComments(application.id);
|
||||
|
||||
@@ -92,12 +140,20 @@ router.get('/me', [requireLogin], async (req, res) => {
|
||||
|
||||
return res.status(200).json(output);
|
||||
} catch (error) {
|
||||
console.error('Failed to load application:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to load application',
|
||||
{
|
||||
user: userID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
return res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
|
||||
// GET /application/:id
|
||||
// GET /me/:id
|
||||
router.get('/me/:id', [requireLogin], async (req: Request, res: Response) => {
|
||||
let appID = Number(req.params.id);
|
||||
let member = req.user.id;
|
||||
@@ -117,9 +173,18 @@ router.get('/me/:id', [requireLogin], async (req: Request, res: Response) => {
|
||||
}
|
||||
return res.status(200).json(output);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Query failed:', err);
|
||||
return res.status(500).json({ error: 'Failed to load application' });
|
||||
catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to load application',
|
||||
{
|
||||
application: appID,
|
||||
user: member,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
return res.status(500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -141,9 +206,17 @@ router.get('/:id', [requireLogin, requireRole("Recruiter")], async (req: Request
|
||||
}
|
||||
return res.status(200).json(output);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Query failed:', err);
|
||||
return res.status(500).json({ error: 'Failed to load application' });
|
||||
catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to load application',
|
||||
{
|
||||
application: appID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
return res.status(500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,9 +234,22 @@ router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req
|
||||
|
||||
await pool.query('CALL sp_accept_new_recruit_validation(?, ?, ?, ?)', [Number(process.env.CONFIG_ID), app.member_id, approved_by, approved_by])
|
||||
|
||||
logger.info('app', "Member application approved", {
|
||||
application: app.id,
|
||||
applicant: app.member_id,
|
||||
approver: approved_by
|
||||
})
|
||||
res.sendStatus(200);
|
||||
} catch (err) {
|
||||
console.error('Approve failed:', err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to approve application',
|
||||
{
|
||||
application: appID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Failed to approve application' });
|
||||
}
|
||||
});
|
||||
@@ -177,9 +263,23 @@ router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: R
|
||||
const app = await getApplicationByID(appID);
|
||||
await denyApplication(appID, approver);
|
||||
await setUserState(app.member_id, MemberState.Denied);
|
||||
|
||||
logger.info('app', "Member application approved", {
|
||||
application: app.id,
|
||||
applicant: app.member_id,
|
||||
approver: approver
|
||||
})
|
||||
res.sendStatus(200);
|
||||
} catch (err) {
|
||||
console.error('Approve failed:', err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to deny application',
|
||||
{
|
||||
application: appID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Failed to deny application' });
|
||||
}
|
||||
});
|
||||
@@ -217,10 +317,25 @@ VALUES(?, ?, ?);`
|
||||
INNER JOIN members AS member ON member.id = app.poster_id
|
||||
WHERE app.id = ?; `;
|
||||
const comment = await conn.query(getSQL, [result.insertId])
|
||||
|
||||
logger.info('app', "Application comment posted", {
|
||||
application: appID,
|
||||
poster: user.id,
|
||||
comment: result.insertId,
|
||||
})
|
||||
|
||||
res.status(201).json(comment[0]);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Comment failed:', err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to post comment',
|
||||
{
|
||||
application: appID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Could not post comment' });
|
||||
} finally {
|
||||
conn.release();
|
||||
@@ -261,10 +376,24 @@ VALUES(?, ?, ?, 1);`
|
||||
INNER JOIN members AS member ON member.id = app.poster_id
|
||||
WHERE app.id = ?; `;
|
||||
const comment = await conn.query(getSQL, [result.insertId])
|
||||
res.status(201).json(comment[0]);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Comment failed:', err);
|
||||
logger.info('app', "Admin application comment posted", {
|
||||
application: appID,
|
||||
poster: user.id,
|
||||
comment: result.insertId,
|
||||
})
|
||||
|
||||
res.status(201).json(comment[0]);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to post comment',
|
||||
{
|
||||
application: appID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Could not post comment' });
|
||||
} finally {
|
||||
conn.release();
|
||||
@@ -275,9 +404,22 @@ router.post('/restart', async (req: Request, res: Response) => {
|
||||
const user = req.user.id;
|
||||
try {
|
||||
await setUserState(user, MemberState.Guest);
|
||||
|
||||
logger.info('app', "Member restarted application", {
|
||||
user: user
|
||||
})
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error('Comment failed:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to restart application',
|
||||
{
|
||||
user: user,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Could not rester application' });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const passport = require('passport');
|
||||
const OpenIDConnectStrategy = require('passport-openidconnect');
|
||||
const dotenv = require('dotenv');
|
||||
dotenv.config();
|
||||
|
||||
const express = require('express');
|
||||
const { param } = require('./applications');
|
||||
@@ -9,11 +7,14 @@ 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, mapDiscordtoID } from '../services/memberService';
|
||||
import { getUserRoles } from '../services/db/rolesService';
|
||||
import { getUserState, mapDiscordtoID } from '../services/db/memberService';
|
||||
import { MemberState } from '@app/shared/types/member';
|
||||
import { toDateTime } from '@app/shared/utils/time';
|
||||
import { logger } from '../services/logging/logger';
|
||||
const querystring = require('querystring');
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
|
||||
function parseJwt(token) {
|
||||
return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
||||
@@ -37,10 +38,10 @@ passport.use(new OpenIDConnectStrategy({
|
||||
// console.log('profile:', profile);
|
||||
// console.log('jwt: ', parseJwt(jwtClaims));
|
||||
// console.log('params:', params);
|
||||
|
||||
let con;
|
||||
|
||||
try {
|
||||
var con = await pool.getConnection();
|
||||
con = await pool.getConnection();
|
||||
|
||||
await con.beginTransaction();
|
||||
|
||||
@@ -49,7 +50,13 @@ passport.use(new OpenIDConnectStrategy({
|
||||
let memberId: number | null = null;
|
||||
//if member exists
|
||||
if (existing.length > 0) {
|
||||
//login
|
||||
memberId = existing[0].id;
|
||||
logger.info('auth', `Existing member login`, {
|
||||
memberId,
|
||||
issuer,
|
||||
});
|
||||
|
||||
} else {
|
||||
//otherwise: create account mode
|
||||
const jwt = parseJwt(jwtClaims);
|
||||
@@ -61,13 +68,17 @@ passport.use(new OpenIDConnectStrategy({
|
||||
|
||||
if (discordID && memberId) {
|
||||
// claim account
|
||||
console.log("Claiming account");
|
||||
const result = await con.query(
|
||||
`UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`,
|
||||
[sub, issuer, memberId]
|
||||
)
|
||||
logger.info('auth', `Existing member claimed via Discord`, {
|
||||
memberId,
|
||||
discordID,
|
||||
issuer,
|
||||
});
|
||||
|
||||
} else {
|
||||
console.log("New Account");
|
||||
// new account
|
||||
const username = sub.username;
|
||||
const result = await con.query(
|
||||
@@ -75,6 +86,13 @@ passport.use(new OpenIDConnectStrategy({
|
||||
[username, sub, issuer]
|
||||
)
|
||||
memberId = Number(result.insertId);
|
||||
|
||||
logger.info('auth', `New member account created`, {
|
||||
memberId,
|
||||
username,
|
||||
issuer,
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +101,26 @@ passport.use(new OpenIDConnectStrategy({
|
||||
await con.commit();
|
||||
return cb(null, { memberId });
|
||||
} catch (error) {
|
||||
await con.rollback();
|
||||
logger.error('auth', `Authentication transaction failed`, {
|
||||
issuer,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
if (con) {
|
||||
try {
|
||||
await con.rollback();
|
||||
} catch (rollbackError) {
|
||||
logger.error('auth', `Rollback failed`, {
|
||||
error: rollbackError instanceof Error
|
||||
? rollbackError.message
|
||||
: String(rollbackError),
|
||||
});
|
||||
}
|
||||
}
|
||||
return cb(error);
|
||||
} finally {
|
||||
con.release();
|
||||
if (con) con.release();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -117,6 +151,7 @@ router.get('/callback', (req, res, next) => {
|
||||
|
||||
router.get('/logout', [requireLogin], function (req, res, next) {
|
||||
req.logout(function (err) {
|
||||
|
||||
if (err) { return next(err); }
|
||||
|
||||
req.session.destroy((err) => {
|
||||
@@ -133,8 +168,8 @@ router.get('/logout', [requireLogin], function (req, res, next) {
|
||||
client_id: process.env.AUTH_CLIENT_ID,
|
||||
returnTo: process.env.CLIENT_URL
|
||||
};
|
||||
res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params));
|
||||
|
||||
res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params));
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -146,34 +181,83 @@ passport.serializeUser(function (user, cb) {
|
||||
});
|
||||
|
||||
passport.deserializeUser(function (user, cb) {
|
||||
const start = performance.now();
|
||||
const timings: Record<string, number> = {};
|
||||
|
||||
process.nextTick(async function () {
|
||||
|
||||
const memberID = user.memberId as number;
|
||||
let con;
|
||||
|
||||
|
||||
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);
|
||||
let t;
|
||||
|
||||
t = performance.now();
|
||||
con = await pool.getConnection();
|
||||
timings.getConnection = performance.now() - t;
|
||||
|
||||
t = performance.now();
|
||||
const userResults = await con.query(
|
||||
`SELECT id, name, discord_id FROM members WHERE id = ?;`,
|
||||
[memberID]
|
||||
);
|
||||
timings.memberQuery = performance.now() - t;
|
||||
|
||||
const userData: {
|
||||
id: number;
|
||||
name: string;
|
||||
roles: Role[];
|
||||
state: MemberState;
|
||||
discord_id?: string;
|
||||
} = userResults[0];
|
||||
|
||||
t = performance.now();
|
||||
const userRoles = await getUserRoles(memberID);
|
||||
timings.roles = performance.now() - t;
|
||||
userData.roles = userRoles || [];
|
||||
|
||||
t = performance.now();
|
||||
userData.state = await getUserState(memberID);
|
||||
timings.state = performance.now() - t;
|
||||
|
||||
// 📊 PROFILING LOG
|
||||
logger.info(
|
||||
'profiling',
|
||||
'passport.deserializeUser completed',
|
||||
{
|
||||
memberId: memberID,
|
||||
total_ms: performance.now() - start,
|
||||
breakdown_ms: timings,
|
||||
},
|
||||
'profiling'
|
||||
);
|
||||
|
||||
return cb(null, userData);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
logger.error(
|
||||
'profiling',
|
||||
'passport.deserializeUser failed',
|
||||
{
|
||||
memberId: memberID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
);
|
||||
return cb(error);
|
||||
|
||||
} finally {
|
||||
con.release();
|
||||
if (con) con.release();
|
||||
}
|
||||
return cb(null, userData);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user: {
|
||||
id: number;
|
||||
name: string;
|
||||
discord_id: string;
|
||||
roles: Role[];
|
||||
state: MemberState;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Request, Response } from "express";
|
||||
import { createEvent, getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus, setEventCancelled, updateEvent } from "../services/calendarService";
|
||||
import { createEvent, getEventAttendance, getEventDetails, getShortEventsInRange, setAttendanceStatus, setEventCancelled, updateEvent } from "../services/db/calendarService";
|
||||
import { CalendarAttendance, CalendarEvent } from "@app/shared/types/calendar";
|
||||
import { requireLogin, requireMemberState, requireRole } from "../middleware/auth";
|
||||
import { MemberState } from "@app/shared/types/member";
|
||||
import { logger } from "../services/logging/logger";
|
||||
|
||||
const express = require('express');
|
||||
const r = express.Router();
|
||||
@@ -28,7 +29,14 @@ r.get('/', async (req, res) => {
|
||||
|
||||
res.status(200).json(events);
|
||||
} catch (error) {
|
||||
console.error('Error fetching calendar events:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get calendar events',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).send('Error fetching calendar events');
|
||||
}
|
||||
});
|
||||
@@ -41,9 +49,21 @@ r.post('/:id/cancel', [requireLogin, requireMemberState(MemberState.Member)], as
|
||||
try {
|
||||
const eventID = Number(req.params.id);
|
||||
setEventCancelled(eventID, true);
|
||||
|
||||
logger.info('app', 'Calendar event cancelled', {
|
||||
event: eventID,
|
||||
user: req.user.id
|
||||
})
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error('Error setting cancel status:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get cancel calendar event',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).send('Error setting cancel status');
|
||||
}
|
||||
})
|
||||
@@ -51,9 +71,21 @@ r.post('/:id/uncancel', [requireLogin, requireMemberState(MemberState.Member)],
|
||||
try {
|
||||
const eventID = Number(req.params.id);
|
||||
setEventCancelled(eventID, false);
|
||||
|
||||
logger.info('app', 'Calendar event un-cancelled', {
|
||||
event: eventID,
|
||||
user: req.user.id
|
||||
})
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error('Error setting cancel status:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to uncancel calendar event',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).send('Error setting cancel status');
|
||||
}
|
||||
})
|
||||
@@ -65,12 +97,27 @@ r.post('/:id/attendance', [requireLogin, requireMemberState(MemberState.Member)]
|
||||
let event = Number(req.params.id);
|
||||
let state = req.query.state as CalendarAttendance;
|
||||
setAttendanceStatus(member, event, state);
|
||||
|
||||
logger.info('app', 'Member set calendar event attendance', {
|
||||
event: event,
|
||||
user: req.user.id,
|
||||
state: state
|
||||
})
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error('Failed to set attendance:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to set attendance',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
|
||||
//get event details
|
||||
r.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -79,9 +126,16 @@ r.get('/:id', async (req: Request, res: Response) => {
|
||||
let details: CalendarEvent = await getEventDetails(eventID);
|
||||
details.eventSignups = await getEventAttendance(eventID);
|
||||
res.status(200).json(details);
|
||||
} catch (err) {
|
||||
console.error('Insert failed:', err);
|
||||
res.status(500).json(err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get calendar event details',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -95,9 +149,22 @@ r.post('/', [requireLogin, requireMemberState(MemberState.Member)], async (req:
|
||||
event.start = new Date(event.start);
|
||||
event.end = new Date(event.end);
|
||||
createEvent(event);
|
||||
|
||||
logger.info('app', 'Calendar event posted', {
|
||||
event: event.id,
|
||||
user: req.user.id
|
||||
})
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error('Failed to create event:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to create calendar event',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
@@ -108,9 +175,22 @@ r.put('/', [requireLogin, requireMemberState(MemberState.Member)], async (req: R
|
||||
event.start = new Date(event.start);
|
||||
event.end = new Date(event.end);
|
||||
updateEvent(event);
|
||||
|
||||
logger.info('app', 'Calendar event updated', {
|
||||
event: event.id,
|
||||
user: req.user.id
|
||||
})
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error('Failed to update event:', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to update calendar event',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course";
|
||||
import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent } from "../services/CourseSerivce";
|
||||
import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent } from "../services/db/CourseSerivce";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { requireLogin, requireMemberState } from "../middleware/auth";
|
||||
import { MemberState } from "@app/shared/types/member";
|
||||
import { logger } from "../services/logging/logger";
|
||||
|
||||
const cr = Router();
|
||||
const er = Router();
|
||||
@@ -16,9 +17,16 @@ cr.get('/', async (req, res) => {
|
||||
try {
|
||||
const courses = await getAllCourses();
|
||||
res.status(200).json(courses);
|
||||
} catch (err) {
|
||||
console.error('failed to fetch courses', err);
|
||||
res.status(500).json('failed to fetch courses\n' + err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to fetch courses',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json('failed to fetch courses');
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,12 +34,20 @@ cr.get('/roles', async (req, res) => {
|
||||
try {
|
||||
const roles = await getCourseEventRoles();
|
||||
res.status(200).json(roles);
|
||||
} catch (err) {
|
||||
console.error('failed to fetch course roles', err);
|
||||
res.status(500).json('failed to fetch course roles\n' + err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to fetch course roles',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json('failed to fetch course roles');
|
||||
}
|
||||
})
|
||||
|
||||
//get event list
|
||||
er.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const allowedSorts = new Map([
|
||||
@@ -55,7 +71,14 @@ er.get('/', async (req: Request, res: Response) => {
|
||||
let events = await getCourseEvents(sortDir, search, page, pageSize);
|
||||
res.status(200).json(events);
|
||||
} catch (error) {
|
||||
console.error('failed to fetch reports', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to fetch course events',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
@@ -65,7 +88,14 @@ er.get('/:id', async (req: Request, res: Response) => {
|
||||
let out = await getCourseEventDetails(Number(req.params.id));
|
||||
res.status(200).json(out);
|
||||
} catch (error) {
|
||||
console.error('failed to fetch report', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to fetch course event',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
@@ -74,9 +104,16 @@ er.get('/attendees/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const attendees: CourseAttendee[] = await getCourseEventAttendees(Number(req.params.id));
|
||||
res.status(200).json(attendees);
|
||||
} catch (err) {
|
||||
console.error('failed to fetch attendees', err);
|
||||
res.status(500).json("failed to fetch attendees\n" + err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to fetch course event attendees',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json("failed to fetch attendees");
|
||||
}
|
||||
})
|
||||
|
||||
@@ -87,9 +124,19 @@ er.post('/', async (req: Request, res: Response) => {
|
||||
data.created_by = posterID;
|
||||
data.event_date = new Date(data.event_date);
|
||||
const id = await insertCourseEvent(data);
|
||||
|
||||
logger.info('app', 'Training report posted', { user: posterID, report: id })
|
||||
res.status(201).json(id);
|
||||
} catch (error) {
|
||||
console.error('failed to post training', error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to post training report',
|
||||
{
|
||||
user: posterID,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json("failed to post training\n" + error)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,22 +3,55 @@ const router = express.Router();
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { requireLogin } from '../middleware/auth';
|
||||
import { logger } from '../services/logging/logger';
|
||||
|
||||
// GET /welcome
|
||||
router.get('/welcome', [requireLogin], async (req: Request, res: Response) => {
|
||||
const output = await fetch(`${process.env.DOC_HOST}/api/pages/717`, {
|
||||
headers: {
|
||||
Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`,
|
||||
}
|
||||
})
|
||||
const t0 = performance.now(); // optional profiling start
|
||||
|
||||
if (output.ok) {
|
||||
const out = await output.json();
|
||||
try {
|
||||
const response = await fetch(`${process.env.DOC_HOST}/api/pages/717`, {
|
||||
headers: {
|
||||
Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
logger.error('app', 'Failed to fetch welcome page from Bookstack', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: text,
|
||||
userId: req.user?.id,
|
||||
});
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
const out = await response.json();
|
||||
res.status(200).json(out.html);
|
||||
} else {
|
||||
console.error("Failed to fetch LOA policy from bookstack");
|
||||
|
||||
// optional profiling log
|
||||
const duration = performance.now() - t0;
|
||||
logger.info(
|
||||
'profiling',
|
||||
'GET /welcome completed',
|
||||
{
|
||||
userId: req.user?.id,
|
||||
total_ms: duration,
|
||||
},
|
||||
'profiling'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('app', 'Error fetching welcome page from Bookstack', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
userId: req.user?.id,
|
||||
});
|
||||
res.sendStatus(500);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
export const docsRouter = router;
|
||||
@@ -3,9 +3,10 @@ const router = express.Router();
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import pool from '../db';
|
||||
import { closeLOA, createNewLOA, getAllLOA, getLOAbyID, getLoaTypes, getUserLOA, setLOAExtension } from '../services/loaService';
|
||||
import { closeLOA, createNewLOA, getAllLOA, getLOAbyID, getLoaTypes, getUserLOA, setLOAExtension } from '../services/db/loaService';
|
||||
import { LOARequest } from '@app/shared/types/loa';
|
||||
import { requireLogin, requireRole } from '../middleware/auth';
|
||||
import { logger } from '../services/logging/logger';
|
||||
|
||||
router.use(requireLogin);
|
||||
|
||||
@@ -18,9 +19,17 @@ router.post("/", async (req: Request, res: Response) => {
|
||||
|
||||
try {
|
||||
await createNewLOA(LOARequest);
|
||||
logger.info('app', 'LOA Posted', { poster: req.user.id, user: LOARequest.member_id })
|
||||
res.sendStatus(201);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to post LOA',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
@@ -32,10 +41,17 @@ router.post("/admin", [requireRole(['17th Administrator', '17th HQ', '17th Comma
|
||||
LOARequest.filed_date = new Date();
|
||||
try {
|
||||
await createNewLOA(LOARequest);
|
||||
logger.info('app', 'LOA Posted', { poster: req.user.id, user: LOARequest.member_id })
|
||||
res.sendStatus(201);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to post LOA',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
); res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,7 +62,14 @@ router.get("/me", async (req: Request, res: Response) => {
|
||||
const result = await getUserLOA(user);
|
||||
res.status(200).json(result)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get user current LOA',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
})
|
||||
@@ -62,7 +85,14 @@ router.get("/history", async (req: Request, res: Response) => {
|
||||
const result = await getUserLOA(user, page, pageSize);
|
||||
res.status(200).json(result)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get user LOA history',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
})
|
||||
@@ -74,7 +104,14 @@ router.get('/all', [requireRole(['17th Administrator', '17th HQ', '17th Command'
|
||||
const result = await getAllLOA(page, pageSize);
|
||||
res.status(200).json(result)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get full LOA history',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
})
|
||||
@@ -84,8 +121,15 @@ router.get('/types', async (req: Request, res: Response) => {
|
||||
let out = await getLoaTypes();
|
||||
res.status(200).json(out);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get LOA types',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
console.error(error);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -99,9 +143,19 @@ router.post('/cancel/:id', async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
await closeLOA(Number(req.params.id), closer);
|
||||
|
||||
logger.info('app', 'LOA Closed', { closed_by: closer, LOA: id })
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to cancel LOA',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
@@ -111,14 +165,24 @@ router.post('/adminCancel/:id', [requireRole(['17th Administrator', '17th HQ', '
|
||||
let closer = req.user.id;
|
||||
try {
|
||||
await closeLOA(Number(req.params.id), closer);
|
||||
|
||||
logger.info('app', 'LOA Closed', { closed_by: closer, LOA: req.params.id })
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to cancel LOA',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: Enforce admin only
|
||||
// extend LOA
|
||||
router.post('/extend/:id', [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => {
|
||||
const to: Date = req.body.to;
|
||||
|
||||
@@ -128,27 +192,71 @@ router.post('/extend/:id', [requireRole(['17th Administrator', '17th HQ', '17th
|
||||
|
||||
try {
|
||||
await setLOAExtension(Number(req.params.id), to);
|
||||
logger.info('app', 'LOA Extended', { extended_by: req.user.id, LOA: req.params.id })
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to extend LOA',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
|
||||
// GET /policy
|
||||
router.get('/policy', async (req: Request, res: Response) => {
|
||||
const output = await fetch(`${process.env.DOC_HOST}/api/pages/42`, {
|
||||
headers: {
|
||||
Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`,
|
||||
}
|
||||
})
|
||||
const t0 = performance.now();
|
||||
|
||||
if (output.ok) {
|
||||
const out = await output.json();
|
||||
try {
|
||||
const response = await fetch(`${process.env.DOC_HOST}/api/pages/42`, {
|
||||
headers: {
|
||||
Authorization: `Token ${process.env.DOC_TOKEN_ID}:${process.env.DOC_TOKEN_SECRET}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
|
||||
logger.error('app', 'Failed to fetch policy page from Bookstack', {
|
||||
pageId: 42,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: text,
|
||||
userId: req.user?.id,
|
||||
});
|
||||
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
const out = await response.json();
|
||||
res.status(200).json(out.html);
|
||||
} else {
|
||||
console.error("Failed to fetch LOA policy from bookstack");
|
||||
|
||||
logger.info(
|
||||
'profiling',
|
||||
'GET /policy completed',
|
||||
{
|
||||
pageId: 42,
|
||||
total_ms: performance.now() - t0,
|
||||
},
|
||||
'profiling'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('app', 'Error fetching policy page from Bookstack', {
|
||||
pageId: 42,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
userId: req.user?.id,
|
||||
});
|
||||
|
||||
res.sendStatus(500);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
export const loaRouter = router;
|
||||
@@ -4,11 +4,14 @@ 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 { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings } from '../services/memberService';
|
||||
import { getUserRoles } from '../services/rolesService';
|
||||
import { getUserActiveLOA } from '../services/db/loaService';
|
||||
import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings } from '../services/db/memberService';
|
||||
import { getUserRoles } from '../services/db/rolesService';
|
||||
import { memberSettings, MemberState, myData } from '@app/shared/types/member';
|
||||
|
||||
import { Performance } from 'perf_hooks';
|
||||
import { logger } from '../services/logging/logger';
|
||||
|
||||
//get all users
|
||||
router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => {
|
||||
try {
|
||||
@@ -26,29 +29,69 @@ router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (r
|
||||
END AS on_loa
|
||||
FROM view_member_rank_unit_status_latest v;`);
|
||||
return res.status(200).json(result);
|
||||
} catch (err) {
|
||||
console.error('Error fetching users:', err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get all users',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
return res.status(500).json({ error: 'Failed to fetch users' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/me', [requireLogin], async (req, res) => {
|
||||
if (req.user === undefined)
|
||||
return res.sendStatus(401)
|
||||
router.get('/me', [requireLogin], async (req: Request, res) => {
|
||||
if (!req.user) return res.sendStatus(401);
|
||||
|
||||
const routeStart = performance.now();
|
||||
const timings: Record<string, number> = {};
|
||||
|
||||
try {
|
||||
const memData = await getUserData(req.user.id);
|
||||
const LOAData = await getUserActiveLOA(req.user.id);
|
||||
const memState = await getUserState(req.user.id);
|
||||
const roleData = await getUserRoles(req.user.id);
|
||||
let t;
|
||||
|
||||
t = performance.now();
|
||||
const memData = await getUserData(req.user.id);
|
||||
timings.member = performance.now() - t;
|
||||
|
||||
t = performance.now();
|
||||
const LOAData = await getUserActiveLOA(req.user.id);
|
||||
timings.loa = performance.now() - t;
|
||||
|
||||
t = performance.now();
|
||||
const memState = await getUserState(req.user.id);
|
||||
timings.state = performance.now() - t;
|
||||
|
||||
t = performance.now();
|
||||
const roleData = await getUserRoles(req.user.id);
|
||||
timings.roles = performance.now() - t;
|
||||
|
||||
const userDataFull: myData = {
|
||||
member: memData,
|
||||
LOAs: LOAData,
|
||||
roles: roleData,
|
||||
state: memState,
|
||||
};
|
||||
|
||||
const userDataFull: myData = { member: memData, LOAs: LOAData, roles: roleData, state: memState };
|
||||
res.status(200).json(userDataFull);
|
||||
|
||||
logger.info('profiling', 'GET /members/me completed', {
|
||||
userId: req.user.id,
|
||||
total_ms: performance.now() - routeStart,
|
||||
breakdown_ms: timings,
|
||||
}, 'profiling');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
logger.error('profiling', 'GET /members/me failed', {
|
||||
userId: req.user?.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
return res.status(500).json({ error: 'Failed to fetch user data' });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
router.get('/settings', [requireLogin], async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -56,7 +99,14 @@ router.get('/settings', [requireLogin], async (req: Request, res: Response) => {
|
||||
let output = await getMemberSettings(user);
|
||||
res.status(200).json(output);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get member settings',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
@@ -66,10 +116,17 @@ router.put('/settings', [requireLogin], async (req: Request, res: Response) => {
|
||||
let user = req.user.id;
|
||||
let settings: memberSettings = req.body;
|
||||
await setUserSettings(user, settings);
|
||||
logger.info('app', 'User updated profile settings', { user: user })
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to update user settings',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
); res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -78,7 +135,14 @@ router.get('/lite', [requireLogin], async (req: Request, res: Response) => {
|
||||
let out = await getAllMembersLite();
|
||||
res.status(200).json(out);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get lite users',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
@@ -89,7 +153,14 @@ router.post('/lite/bulk', async (req: Request, res: Response) => {
|
||||
let out = await getMembersLite(ids);
|
||||
res.status(200).json(out);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get batch lite users',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
@@ -101,22 +172,36 @@ router.post('/full/bulk', async (req: Request, res: Response) => {
|
||||
let out = await getMembersFull(ids);
|
||||
res.status(200).json(out);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json(error);
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get batch full users',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
); res.status(500).json(error);
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:id', [requireLogin], async (req, res) => {
|
||||
const userId = req.params.id;
|
||||
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
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' });
|
||||
}
|
||||
return res.status(200).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching user:', err);
|
||||
return res.status(500).json({ error: 'Failed to fetch user' });
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get user',
|
||||
{
|
||||
user: userId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
); return res.status(500).json({ error: 'Failed to fetch user' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MemberState } from "@app/shared/types/member";
|
||||
import { requireLogin, requireMemberState, requireRole } from "../middleware/auth";
|
||||
import { getAllRanks, insertMemberRank } from "../services/rankService";
|
||||
import { batchInsertMemberRank, getAllRanks, getPromotionHistorySummary, getPromotionsOnDay, insertMemberRank } from "../services/db/rankService";
|
||||
import { BatchPromotion, BatchPromotionMember } from '@app/shared/schemas/promotionSchema'
|
||||
|
||||
import express = require('express');
|
||||
import { logger } from "../services/logging/logger";
|
||||
const r = express.Router();
|
||||
const ur = express.Router();
|
||||
|
||||
@@ -11,26 +13,81 @@ r.use(requireLogin)
|
||||
ur.use(requireLogin)
|
||||
|
||||
//insert a new latest rank for a user
|
||||
ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req, res) => {
|
||||
3
|
||||
ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const change = req.body?.change;
|
||||
await insertMemberRank(change.member_id, change.rank_id, change.date);
|
||||
const change = req.body.promotions as BatchPromotionMember[];
|
||||
const approver = req.body.approver as number;
|
||||
const author = req.user.id;
|
||||
if (!change) res.sendStatus(400);
|
||||
|
||||
await batchInsertMemberRank(change, author, approver);
|
||||
logger.info('app', 'Promotion batch submitted', { author: author })
|
||||
res.sendStatus(201);
|
||||
} catch (err) {
|
||||
console.error('Insert failed:', err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to post rank change',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Failed to update ranks' });
|
||||
}
|
||||
});
|
||||
|
||||
ur.get('/', async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const promos = await getPromotionHistorySummary();
|
||||
res.status(200).json(promos);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get rank change history',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
ur.get('/:day', async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
if (!req.params.day) res.sendStatus(400);
|
||||
|
||||
var day = new Date(req.params.day)
|
||||
const promos = await getPromotionsOnDay(day);
|
||||
res.status(200).json(promos);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get rank change history on day',
|
||||
{
|
||||
day: day,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
})
|
||||
|
||||
//get all ranks
|
||||
r.get('/', async (req, res) => {
|
||||
try {
|
||||
const ranks = await getAllRanks();
|
||||
res.json(ranks);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get all ranks',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,84 +5,134 @@ 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';
|
||||
import { assignUserGroup, createGroup, getAllRoles, getRole, getUsersWithRole } from '../services/db/rolesService';
|
||||
import { Request, Response } from 'express';
|
||||
import { logger } from '../services/logging/logger';
|
||||
|
||||
r.use(requireLogin)
|
||||
ur.use(requireLogin)
|
||||
|
||||
//manually assign a member to a group
|
||||
ur.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
|
||||
ur.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res) => {
|
||||
const body = req.body;
|
||||
|
||||
try {
|
||||
const body = req.body;
|
||||
|
||||
assignUserGroup(body.member_id, body.role_id);
|
||||
await assignUserGroup(body.member_id, body.role_id);
|
||||
|
||||
logger.info('app', 'User assigned role', { user: body.member_id, role: body.role_id, assigner: req.user.id })
|
||||
res.sendStatus(201);
|
||||
} catch (err) {
|
||||
console.error('Insert failed:', err);
|
||||
} catch (error) {
|
||||
if (error?.code === 'ER_DUP_ENTRY') {
|
||||
return res.status(400).json({
|
||||
error: 'Member already has this role',
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to assign role',
|
||||
{
|
||||
user: body.member_id,
|
||||
role: body.role_id,
|
||||
assigner: req.user.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Failed to add to group' });
|
||||
}
|
||||
});
|
||||
|
||||
//manually remove member from group
|
||||
ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
|
||||
ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => {
|
||||
const body = req.body;
|
||||
|
||||
try {
|
||||
const body = req.body;
|
||||
|
||||
const sql = 'DELETE FROM members_roles WHERE member_id = ? AND role_id = ?'
|
||||
await pool.query(sql, [body.member_id, body.role_id])
|
||||
|
||||
logger.info('app', 'User removed role', { user: body.member_id, role: body.role_id, assigner: req.user.id })
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
||||
catch (err) {
|
||||
console.error("delete failed: ", err)
|
||||
catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to remove role',
|
||||
{
|
||||
user: body.member_id,
|
||||
role: body.role_id,
|
||||
assigner: req.user.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.status(500).json({ error: 'Failed to remove from group' });
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
//get all roles
|
||||
r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => {
|
||||
try {
|
||||
var con = await pool.getConnection();
|
||||
|
||||
// Get all roles
|
||||
const roles = await con.query('SELECT * FROM roles;');
|
||||
|
||||
// Get all members for each role
|
||||
const membersRoles = await con.query(`
|
||||
SELECT mr.role_id, v.*
|
||||
FROM members_roles mr
|
||||
JOIN view_member_rank_unit_status_latest v ON mr.member_id = v.member_id
|
||||
`);
|
||||
|
||||
|
||||
// Group members by role_id
|
||||
const roleIdToMembers = {};
|
||||
for (const row of membersRoles) {
|
||||
if (!roleIdToMembers[row.role_id]) roleIdToMembers[row.role_id] = [];
|
||||
// Remove role_id from member object
|
||||
const { role_id, ...member } = row;
|
||||
roleIdToMembers[role_id].push(member);
|
||||
}
|
||||
|
||||
// Attach members to each role
|
||||
const result = roles.map(role => ({
|
||||
...role,
|
||||
members: roleIdToMembers[role.id] || []
|
||||
}));
|
||||
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
} finally {
|
||||
con.release();
|
||||
const roles = await getAllRoles();
|
||||
|
||||
res.status(200).json(roles);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get all roles',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
r.get('/:id/members', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const members = await getUsersWithRole(Number(req.params.id));
|
||||
res.status(200).json(members);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get role members',
|
||||
{
|
||||
role: req.params.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
r.get('/:id', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const role = await getRole(Number(req.params.id));
|
||||
res.status(200).json(role);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get role members',
|
||||
{
|
||||
role: req.params.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
//create a new role
|
||||
r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
|
||||
r.post('/', [requireMemberState(MemberState.Member), requireRole("dev")], async (req, res) => {
|
||||
try {
|
||||
const { name, color, description } = req.body;
|
||||
if (!name || !color) {
|
||||
@@ -103,7 +153,7 @@ r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administr
|
||||
}
|
||||
})
|
||||
|
||||
r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => {
|
||||
r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("dev")], async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const memberStatusR = express.Router();
|
||||
|
||||
import pool from '../db';
|
||||
import { requireLogin } from '../middleware/auth';
|
||||
import { logger } from '../services/logging/logger';
|
||||
|
||||
statusR.use(requireLogin);
|
||||
memberStatusR.use(requireLogin);
|
||||
@@ -38,9 +39,16 @@ statusR.get('/', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM statuses;');
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'app',
|
||||
'Failed to get all statuses',
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
}
|
||||
);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import pool from "../db"
|
||||
import pool from "../../db"
|
||||
import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails, CourseEventSummary, RawAttendeeRow } from "@app/shared/types/course"
|
||||
import { PagedData } from "@app/shared/types/pagination";
|
||||
import { toDateTime } from "@app/shared/utils/time";
|
||||
@@ -1,11 +1,20 @@
|
||||
import { ApplicationListRow, ApplicationRow, CommentRow } from "@app/shared/types/application";
|
||||
import pool from "../db";
|
||||
import pool from "../../db";
|
||||
import { error } from "console";
|
||||
|
||||
export async function createApplication(memberID: number, appVersion: number, app: string) {
|
||||
/**
|
||||
* Create an application in the db
|
||||
* @param memberID
|
||||
* @param appVersion
|
||||
* @param app
|
||||
* @returns ID of the created application
|
||||
*/
|
||||
export async function createApplication(memberID: number, appVersion: number, app: string): Promise<number> {
|
||||
const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`;
|
||||
const params = [memberID, appVersion, JSON.stringify(app)]
|
||||
return await pool.query(sql, params);
|
||||
|
||||
let result = await pool.query(sql, params);
|
||||
return Number(result.insertId);
|
||||
}
|
||||
|
||||
export async function getMemberApplication(memberID: number): Promise<ApplicationRow> {
|
||||
@@ -1,4 +1,4 @@
|
||||
import pool from '../db';
|
||||
import pool from '../../db';
|
||||
import { CalendarEventShort, CalendarSignup, CalendarEvent, CalendarAttendance } from "@app/shared/types/calendar"
|
||||
import { toDateTime } from "@app/shared/utils/time"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toDateTime } from "@app/shared/utils/time";
|
||||
import pool from "../db";
|
||||
import pool from "../../db";
|
||||
import { LOARequest, LOAType } from '@app/shared/types/loa'
|
||||
import { PagedData } from '@app/shared/types/pagination'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pool from "../db";
|
||||
import { Member, MemberLight, memberSettings, MemberState } from '@app/shared/types/member'
|
||||
import { Role } from "@app/shared/types/roles";
|
||||
import pool from "../../db";
|
||||
import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState } from '@app/shared/types/member'
|
||||
|
||||
export async function getUserData(userID: number): Promise<Member> {
|
||||
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`;
|
||||
@@ -60,10 +61,50 @@ export async function getAllMembersLite(): Promise<MemberLight[]> {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getMembersFull(ids: number[]): Promise<Member[]> {
|
||||
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id IN (?);`;
|
||||
const res: Member[] = await pool.query(sql, [ids]);
|
||||
return res;
|
||||
export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]> {
|
||||
const sql = `
|
||||
SELECT m.*,
|
||||
COALESCE(
|
||||
JSON_ARRAYAGG(
|
||||
CASE
|
||||
WHEN r.id IS NOT NULL THEN JSON_OBJECT(
|
||||
'id', r.id,
|
||||
'name', r.name,
|
||||
'color', r.color,
|
||||
'description', r.description
|
||||
)
|
||||
END
|
||||
),
|
||||
JSON_ARRAY()
|
||||
) AS roles
|
||||
FROM view_member_rank_unit_status_latest m
|
||||
LEFT JOIN members_roles mr ON m.member_id = mr.member_id
|
||||
LEFT JOIN roles r ON mr.role_id = r.id
|
||||
WHERE m.member_id IN (?)
|
||||
GROUP BY m.member_id;
|
||||
`;
|
||||
|
||||
const rows: any[] = await pool.query(sql, [ids]);
|
||||
|
||||
return rows.map(row => {
|
||||
const member: Member = {
|
||||
member_id: row.member_id,
|
||||
member_name: row.member_name,
|
||||
displayName: row.displayName,
|
||||
rank: row.rank,
|
||||
rank_date: row.rank_date,
|
||||
unit: row.unit,
|
||||
unit_date: row.unit_date,
|
||||
status: row.status,
|
||||
status_date: row.status_date,
|
||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||
};
|
||||
|
||||
// roles comes as array of strings; parse each one
|
||||
const roles: Role[] = JSON.parse(row.roles).map((r: string) => JSON.parse(r));
|
||||
|
||||
return { member, roles };
|
||||
});
|
||||
}
|
||||
|
||||
export async function mapDiscordtoID(id: number): Promise<number | null> {
|
||||
108
api/src/services/db/rankService.ts
Normal file
108
api/src/services/db/rankService.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { BatchPromotion, BatchPromotionMember } from "@app/shared/schemas/promotionSchema";
|
||||
import { PromotionDetails, PromotionSummary } from "@app/shared/types/rank"
|
||||
import pool from "../../db";
|
||||
import { PagedData } from "@app/shared/types/pagination";
|
||||
import { toDate, toDateIgnoreZone, toDateTime } from "@app/shared/utils/time";
|
||||
|
||||
export async function getAllRanks() {
|
||||
const rows = await pool.query(
|
||||
'SELECT id, name, short_name, sort_id FROM ranks;'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function getRankByName(name: string) {
|
||||
const rows = await pool.query(`SELECT id, name, short_name, sort_id FROM ranks WHERE name = ?`, [name]);
|
||||
|
||||
if (rows.length === 0)
|
||||
throw new Error("Could not find rank: " + name);
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
export async function insertMemberRank(member_id: number, rank_id: number, date: Date): Promise<void>;
|
||||
export async function insertMemberRank(member_id: number, rank_id: number): Promise<void>;
|
||||
|
||||
export async function insertMemberRank(member_id: number, rank_id: number, date?: Date): Promise<void> {
|
||||
const sql = date
|
||||
? `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]
|
||||
: [member_id, rank_id];
|
||||
|
||||
await pool.query(sql, params);
|
||||
}
|
||||
|
||||
|
||||
export async function batchInsertMemberRank(promos: BatchPromotionMember[], author: number, approver: number) {
|
||||
try {
|
||||
var con = await pool.getConnection();
|
||||
promos.forEach(p => {
|
||||
con.query(`CALL sp_update_member_rank(?, ?, ?, ?, ?, ?)`, [p.member_id, p.rank_id, approver, author, "Rank Change", toDateIgnoreZone(new Date(p.start_date))])
|
||||
});
|
||||
|
||||
con.commit();
|
||||
return
|
||||
} catch (error) {
|
||||
throw error; //pass it up
|
||||
} finally {
|
||||
con.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPromotionHistorySummary(page: number = 1, pageSize: number = 15): Promise<PagedData<PromotionSummary>> {
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
let sql = `SELECT
|
||||
DATE(start_date) AS entry_day
|
||||
FROM
|
||||
members_ranks
|
||||
WHERE reason = 'Rank Change'
|
||||
GROUP BY
|
||||
entry_day
|
||||
ORDER BY
|
||||
entry_day DESC
|
||||
LIMIT ? OFFSET ?;`
|
||||
|
||||
let promoList: PromotionSummary[] = await pool.query(sql, [pageSize, offset]) as PromotionSummary[];
|
||||
|
||||
let rowCount = Number((await pool.query(`SELECT
|
||||
COUNT(*) AS total_grouped_days_count
|
||||
FROM
|
||||
(
|
||||
SELECT DISTINCT DATE(start_date)
|
||||
FROM members_ranks
|
||||
WHERE reason = 'Rank Change'
|
||||
) AS grouped_days;`))[0]);
|
||||
|
||||
let pageCount = rowCount / pageSize;
|
||||
|
||||
let output: PagedData<PromotionSummary> = { data: promoList, pagination: { page: page, pageSize: pageSize, total: rowCount, totalPages: pageCount } }
|
||||
return output;
|
||||
}
|
||||
|
||||
export async function getPromotionsOnDay(day: Date): Promise<PromotionDetails[]> {
|
||||
|
||||
const dayString = toDateTime(day);
|
||||
|
||||
// SQL query to fetch all records from members_unit for the specified day
|
||||
let sql = `
|
||||
SELECT
|
||||
mr.id AS promo_id,
|
||||
mr.member_id,
|
||||
mr.created_by_id,
|
||||
mr.authorized_by_id,
|
||||
r.short_name
|
||||
FROM members_ranks AS mr
|
||||
LEFT JOIN ranks AS r ON r.id = mr.rank_id
|
||||
WHERE DATE(mr.start_date) = ? && mr.reason = 'Rank Change'
|
||||
ORDER BY mr.start_date ASC;
|
||||
`;
|
||||
|
||||
let batchPromotion = await pool.query(sql, [dayString]) as PromotionDetails[];
|
||||
|
||||
return batchPromotion;
|
||||
}
|
||||
57
api/src/services/db/rolesService.ts
Normal file
57
api/src/services/db/rolesService.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { MemberLight } from '@app/shared/types/member';
|
||||
import pool from '../../db';
|
||||
import { Role, RoleSummary } from '@app/shared/types/roles'
|
||||
|
||||
export async function assignUserGroup(userID: number, roleID: number) {
|
||||
const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`;
|
||||
const params = [userID, roleID];
|
||||
|
||||
return await pool.query(sql, params);
|
||||
}
|
||||
|
||||
export async function createGroup(name: string, color: string, description: string) {
|
||||
const sql = `INSERT INTO roles (name, color, description) VALUES (?, ?, ?)`;
|
||||
const params = [name, color, description];
|
||||
|
||||
const result = await pool.query(sql, params);
|
||||
return { id: result.insertId, name, color, description };
|
||||
}
|
||||
|
||||
export async function getUserRoles(userID: number): Promise<Role[]> {
|
||||
const sql = `SELECT r.id, r.name
|
||||
FROM members_roles mr
|
||||
INNER JOIN roles r ON mr.role_id = r.id
|
||||
WHERE mr.member_id = ?;`;
|
||||
|
||||
return await pool.query(sql, [userID]);
|
||||
}
|
||||
|
||||
export async function getRole(id: number): Promise<Role> {
|
||||
let res = await pool.query(`SELECT * FROM roles WHERE id = ?`, [id])
|
||||
return res[0] as Role;
|
||||
}
|
||||
|
||||
export async function getAllRoles(): Promise<RoleSummary> {
|
||||
return await pool.query(`SELECT id, name, color FROM roles`);
|
||||
}
|
||||
|
||||
export async function getUsersWithRole(roleId: number): Promise<MemberLight[]> {
|
||||
const out = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
m.member_id AS id,
|
||||
m.member_name AS username,
|
||||
m.displayName,
|
||||
u.color
|
||||
FROM members_roles mr
|
||||
JOIN view_member_rank_unit_status_latest m
|
||||
ON m.member_id = mr.member_id
|
||||
LEFT JOIN units u
|
||||
ON u.name = m.unit
|
||||
WHERE mr.role_id = ?
|
||||
`,
|
||||
[roleId]
|
||||
)
|
||||
|
||||
return out as MemberLight[]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import pool from "../db"
|
||||
import pool from "../../db"
|
||||
|
||||
export async function assignUserToStatus(userID: number, statusID: number) {
|
||||
const sql = `INSERT INTO members_statuses (member_id, status_id, start_date) VALUES (?, ?, NOW())`
|
||||
56
api/src/services/events/eventBus.ts
Normal file
56
api/src/services/events/eventBus.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { logger } from "../logging/logger";
|
||||
|
||||
interface Event {
|
||||
id: string
|
||||
type: string
|
||||
occurredAt: string
|
||||
payload?: Record<string, any>
|
||||
}
|
||||
|
||||
type EventHandler = (event: Event) => void | Promise<void>;
|
||||
|
||||
class EventBus {
|
||||
private handlers: Map<string, EventHandler[]> = new Map();
|
||||
|
||||
/**
|
||||
* Register event listener
|
||||
* @param type
|
||||
* @param handler
|
||||
*/
|
||||
on(type: string, handler: EventHandler) {
|
||||
const handlers = this.handlers.get(type) ?? [];
|
||||
handlers.push(handler);
|
||||
this.handlers.set(type, handlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event of given type
|
||||
* @param type
|
||||
* @param payload
|
||||
*/
|
||||
async emit(type: string, payload?: Record<string, any>) {
|
||||
const event: Event = {
|
||||
id: randomUUID(),
|
||||
type,
|
||||
occurredAt: new Date().toISOString(),
|
||||
payload
|
||||
}
|
||||
|
||||
const handlers = this.handlers.get(type) ?? []
|
||||
|
||||
for (const h of handlers) {
|
||||
try {
|
||||
await h(event)
|
||||
} catch (error) {
|
||||
logger.error('app', 'Event handler failed', {
|
||||
type: event.type,
|
||||
id: event.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bus = new EventBus();
|
||||
39
api/src/services/integrations/discord.ts
Normal file
39
api/src/services/integrations/discord.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { bus } from "../events/eventBus";
|
||||
import { logger } from "../logging/logger";
|
||||
|
||||
export function initializeDiscordIntegrations() {
|
||||
bus.on('application.create', async (event) => {
|
||||
|
||||
if (!process.env.DISCORD_APPLICATIONS_WEBHOOK) {
|
||||
logger.error("app", 'Discord Applications Webhook is not defined')
|
||||
return;
|
||||
}
|
||||
|
||||
let applicantName = event.payload.member_discord_id || event.payload.member_name;
|
||||
if (event.payload.member_discord_id) {
|
||||
applicantName = `<@${event.payload.member_discord_id}>`;
|
||||
}
|
||||
const link = `${process.env.CLIENT_URL}/administration/applications/${event.payload.application}`;
|
||||
|
||||
const embed = {
|
||||
title: "Application Posted",
|
||||
description: `[View Application](${link})`,
|
||||
color: 0x00ff00, // optional: green color
|
||||
timestamp: new Date().toISOString(), // <-- Discord expects ISO8601
|
||||
fields: [
|
||||
{
|
||||
name: "Submitted By",
|
||||
value: applicantName,
|
||||
inline: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// send to Discord webhook
|
||||
await fetch(process.env.DISCORD_APPLICATIONS_WEBHOOK!, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ embeds: [embed] }),
|
||||
});
|
||||
});
|
||||
}
|
||||
72
api/src/services/logging/logger.ts
Normal file
72
api/src/services/logging/logger.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
export type LogDepth = 'normal' | 'verbose' | 'profiling';
|
||||
export type LogType = 'http' | 'app' | 'auth' | 'profiling';
|
||||
|
||||
export interface LogHeader {
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
depth: LogDepth;
|
||||
type: LogType; // 'http', 'app', 'db', etc.
|
||||
user_id?: number;
|
||||
}
|
||||
|
||||
export interface LogPayload {
|
||||
message?: string; // short human-friendly description
|
||||
data?: Record<string, any>; // type-specific rich data
|
||||
}
|
||||
|
||||
// Environment defaults
|
||||
const CURRENT_DEPTH: LogDepth = (process.env.LOG_DEPTH as LogDepth) || 'normal';
|
||||
|
||||
const DEPTH_ORDER: Record<LogDepth, number> = { normal: 0, verbose: 1, profiling: 2 };
|
||||
|
||||
function shouldLog(depth: LogDepth) {
|
||||
let should = DEPTH_ORDER[depth] <= DEPTH_ORDER[CURRENT_DEPTH]
|
||||
return should;
|
||||
}
|
||||
|
||||
function emitLog(header: LogHeader, payload: LogPayload = {}) {
|
||||
if (!shouldLog(header.depth)) return;
|
||||
|
||||
const logLine = { ...header, ...payload };
|
||||
|
||||
if (header.level === 'error')
|
||||
console.error(JSON.stringify(logLine))
|
||||
else
|
||||
console.log(JSON.stringify(logLine));
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
log(level: LogLevel, type: LogType, message: string, data?: Record<string, any>, depth: LogDepth = 'normal', context?: Partial<LogHeader>) {
|
||||
const header: LogHeader = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
depth,
|
||||
type,
|
||||
...context,
|
||||
};
|
||||
|
||||
const payload: LogPayload = {
|
||||
message,
|
||||
data,
|
||||
};
|
||||
|
||||
emitLog(header, payload);
|
||||
},
|
||||
|
||||
info(type: LogType, message: string, data?: Record<string, any>, depth: LogDepth = 'normal', context?: Partial<LogHeader>) {
|
||||
this.log('info', type, message, data, depth, context);
|
||||
},
|
||||
|
||||
debug(type: LogType, message: string, data?: Record<string, any>, depth: LogDepth = 'normal', context?: Partial<LogHeader>) {
|
||||
this.log('debug', type, message, data, depth, context);
|
||||
},
|
||||
|
||||
warn(type: LogType, message: string, data?: Record<string, any>, depth: LogDepth = 'normal', context?: Partial<LogHeader>) {
|
||||
this.log('warn', type, message, data, depth, context);
|
||||
},
|
||||
|
||||
error(type: LogType, message: string, data?: Record<string, any>, depth: LogDepth = 'normal', context?: Partial<LogHeader>) {
|
||||
this.log('error', type, message, data, depth, context);
|
||||
},
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import pool from "../db";
|
||||
|
||||
export async function getAllRanks() {
|
||||
const rows = await pool.query(
|
||||
'SELECT id, name, short_name, sort_id FROM ranks;'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function getRankByName(name: string) {
|
||||
const rows = await pool.query(`SELECT id, name, short_name, sort_id FROM ranks WHERE name = ?`, [name]);
|
||||
|
||||
if (rows.length === 0)
|
||||
throw new Error("Could not find rank: " + name);
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
export async function insertMemberRank(member_id: number, rank_id: number, date: Date): Promise<void>;
|
||||
export async function insertMemberRank(member_id: number, rank_id: number): Promise<void>;
|
||||
|
||||
export async function insertMemberRank(member_id: number, rank_id: number, date?: Date): Promise<void> {
|
||||
const sql = date
|
||||
? `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]
|
||||
: [member_id, rank_id];
|
||||
|
||||
await pool.query(sql, params);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import pool from '../db';
|
||||
import { Role } from '@app/shared/types/roles'
|
||||
|
||||
export async function assignUserGroup(userID: number, roleID: number) {
|
||||
|
||||
const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`;
|
||||
const params = [userID, roleID];
|
||||
|
||||
return await pool.query(sql, params);
|
||||
}
|
||||
|
||||
export async function createGroup(name: string, color: string, description: string) {
|
||||
const sql = `INSERT INTO roles (name, color, description) VALUES (?, ?, ?)`;
|
||||
const params = [name, color, description];
|
||||
|
||||
const result = await pool.query(sql, params);
|
||||
return { id: result.insertId, name, color, description };
|
||||
}
|
||||
|
||||
export async function getUserRoles(userID: number): Promise<Role[]> {
|
||||
const sql = `SELECT r.id, r.name
|
||||
FROM members_roles mr
|
||||
INNER JOIN roles r ON mr.role_id = r.id
|
||||
WHERE mr.member_id = ?;`;
|
||||
|
||||
return await pool.query(sql, [userID]);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"node",
|
||||
"express"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"paths": {
|
||||
"@app/shared/*": ["../shared/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user