diff --git a/api/.env.example b/api/.env.example index 9a1e8da..710756b 100644 --- a/api/.env.example +++ b/api/.env.example @@ -23,6 +23,9 @@ APPLICATION_VERSION= # Should match release tag APPLICATION_ENVIRONMENT= # dev / prod CONFIG_ID= # configures +# Logger +LOG_DEPTH= # normal / verbose / profiling + # Glitchtip GLITCHTIP_DSN= DISABLE_GLITCHTIP= # true/false diff --git a/api/src/db.ts b/api/src/db.ts index 3756f73..480dcf4 100644 --- a/api/src/db.ts +++ b/api/src/db.ts @@ -1,8 +1,5 @@ // const mariadb = require('mariadb') import * as mariadb from 'mariadb'; -const dotenv = require('dotenv') -dotenv.config(); - const pool = mariadb.createPool({ host: process.env.DB_HOST, diff --git a/api/src/index.ts b/api/src/index.ts index f5331a8..33f17f1 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -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() app.use(morgan((tokens: morgan.TokenIndexer, req: express.Request, res: express.Response) => { - return JSON.stringify({ + + const head: LogHeader = { type: 'http', + level: 'info', + depth: 'normal', timestamp: new Date().toISOString(), + } - 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)), + 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'], + }, + } - ip: req.ip, - user_agent: req.headers['user-agent'], - - user: req.user - ? { id: req.user.id, name: req.user.name } - : null, - }); + 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 @@ -110,5 +118,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} `) }) diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index 9a1a732..24b0fb2 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -2,59 +2,108 @@ 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'; //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) => { + const memberID = req.user.id; + const App = req.body?.App || {}; + const appVersion = 1; + try { - const App = req.body?.App || {}; - const memberID = req.user.id; + req.profiler?.start('createApplication'); + await createApplication(memberID, appVersion, JSON.stringify(App)); + req.profiler?.end('createApplication'); - const appVersion = 1; - - await createApplication(memberID, appVersion, JSON.stringify(App)) + req.profiler?.start('setUserState'); await setUserState(memberID, MemberState.Applicant); + req.profiler?.end('setUserState'); res.sendStatus(201); + + // Log full route profiling + const summary = req.profiler?.summary(); + if (summary) { + logger.info( + 'profiling', + 'POST /application completed', + { + memberID, + appVersion, + ...summary, + }, + 'profiling' + ); + } } 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 +117,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); } }) @@ -79,7 +136,7 @@ router.get('/me', [requireLogin], async (req, res) => { try { let application = await getMemberApplication(userID); - + if (application === undefined) { res.sendStatus(204); return; @@ -94,12 +151,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; @@ -119,9 +184,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); } }); @@ -143,9 +217,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); } }); @@ -163,9 +245,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' }); } }); @@ -179,9 +274,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' }); } }); @@ -219,10 +328,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(); @@ -263,10 +387,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(); @@ -277,9 +415,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' }); } }) diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index 4925da1..b292811 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -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(); } })); @@ -133,8 +167,12 @@ 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)); + logger.info('auth', `Member logged out`, { + user: req.user.id, + }); + + res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params)); }) }); }); @@ -146,28 +184,75 @@ passport.serializeUser(function (user, cb) { }); passport.deserializeUser(function (user, cb) { + const start = performance.now(); + const timings: Record = {}; + 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 FROM members WHERE id = ?;`, + [memberID] + ); + timings.memberQuery = performance.now() - t; + + const userData: { + id: number; + name: string; + roles: Role[]; + state: MemberState; + } = 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 { diff --git a/api/src/routes/calendar.ts b/api/src/routes/calendar.ts index a5c96be..f5279f7 100644 --- a/api/src/routes/calendar.ts +++ b/api/src/routes/calendar.ts @@ -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); } }) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 91a2057..007d593 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -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) } }) diff --git a/api/src/routes/docs.ts b/api/src/routes/docs.ts index 13ada87..471891a 100644 --- a/api/src/routes/docs.ts +++ b/api/src/routes/docs.ts @@ -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; \ No newline at end of file diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index 65aa115..8555fb7 100644 --- a/api/src/routes/loa.ts +++ b/api/src/routes/loa.ts @@ -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; \ No newline at end of file diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index c73b8f2..f7bdd15 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -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 = {}; 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' }); } }); diff --git a/api/src/routes/ranks.ts b/api/src/routes/ranks.ts index af22a03..8ba4afc 100644 --- a/api/src/routes/ranks.ts +++ b/api/src/routes/ranks.ts @@ -1,9 +1,10 @@ import { MemberState } from "@app/shared/types/member"; import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; -import { batchInsertMemberRank, getAllRanks, getPromotionHistorySummary, getPromotionsOnDay, 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(); @@ -19,10 +20,17 @@ ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), re if (!change) res.sendStatus(400); await batchInsertMemberRank(change, author); - + 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' }); } }); @@ -30,11 +38,17 @@ ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), re ur.get('/', async (req: express.Request, res: express.Response) => { try { const promos = await getPromotionHistorySummary(); - console.log(promos); res.status(200).json(promos); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); + } 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); } }); @@ -42,13 +56,20 @@ ur.get('/:day', async (req: express.Request, res: express.Response) => { try { if (!req.params.day) res.sendStatus(400); - let day = new Date(req.params.day) + var day = new Date(req.params.day) const promos = await getPromotionsOnDay(day); - console.log(promos); res.status(200).json(promos); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); + } 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); } }) @@ -57,8 +78,15 @@ 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' }); } }); diff --git a/api/src/routes/roles.ts b/api/src/routes/roles.ts index d77093f..c6a088e 100644 --- a/api/src/routes/roles.ts +++ b/api/src/routes/roles.ts @@ -5,46 +5,71 @@ 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, getAllRoles, getRole, getUsersWithRole } 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; 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) { - if (err?.code === 'ER_DUP_ENTRY') { + } catch (error) { + if (error?.code === 'ER_DUP_ENTRY') { return res.status(400).json({ error: 'Member already has this role', }); } - console.error('Insert failed:', err); + 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' }); - } }) @@ -52,9 +77,17 @@ ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Admini r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => { try { const roles = await getAllRoles(); + res.status(200).json(roles); - } catch (err) { - console.error(err); + } 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); } }); @@ -63,8 +96,16 @@ r.get('/:id/members', [requireMemberState(MemberState.Member)], async (req: Requ try { const members = await getUsersWithRole(Number(req.params.id)); res.status(200).json(members); - } catch (err) { - console.error(err); + } 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); } }) @@ -74,8 +115,16 @@ r.get('/:id', [requireMemberState(MemberState.Member)], async (req: Request, res try { const role = await getRole(Number(req.params.id)); res.status(200).json(role); - } catch (err) { - console.error(err); + } 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); } }) diff --git a/api/src/routes/statuses.ts b/api/src/routes/statuses.ts index 8457800..3724579 100644 --- a/api/src/routes/statuses.ts +++ b/api/src/routes/statuses.ts @@ -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); } }); diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/db/CourseSerivce.ts similarity index 99% rename from api/src/services/CourseSerivce.ts rename to api/src/services/db/CourseSerivce.ts index d85ffd8..1fdd8bf 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/db/CourseSerivce.ts @@ -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"; diff --git a/api/src/services/applicationService.ts b/api/src/services/db/applicationService.ts similarity index 99% rename from api/src/services/applicationService.ts rename to api/src/services/db/applicationService.ts index 159dbe3..451d01c 100644 --- a/api/src/services/applicationService.ts +++ b/api/src/services/db/applicationService.ts @@ -1,5 +1,5 @@ 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) { diff --git a/api/src/services/calendarService.ts b/api/src/services/db/calendarService.ts similarity index 99% rename from api/src/services/calendarService.ts rename to api/src/services/db/calendarService.ts index 4862650..3c677b5 100644 --- a/api/src/services/calendarService.ts +++ b/api/src/services/db/calendarService.ts @@ -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" diff --git a/api/src/services/loaService.ts b/api/src/services/db/loaService.ts similarity index 99% rename from api/src/services/loaService.ts rename to api/src/services/db/loaService.ts index c2378b9..d6a9c40 100644 --- a/api/src/services/loaService.ts +++ b/api/src/services/db/loaService.ts @@ -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' diff --git a/api/src/services/memberService.ts b/api/src/services/db/memberService.ts similarity index 99% rename from api/src/services/memberService.ts rename to api/src/services/db/memberService.ts index e1f4fed..d1a2db5 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/db/memberService.ts @@ -1,5 +1,5 @@ import { Role } from "@app/shared/types/roles"; -import pool from "../db"; +import pool from "../../db"; import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState } from '@app/shared/types/member' export async function getUserData(userID: number): Promise { diff --git a/api/src/services/rankService.ts b/api/src/services/db/rankService.ts similarity index 93% rename from api/src/services/rankService.ts rename to api/src/services/db/rankService.ts index cc10f94..a826617 100644 --- a/api/src/services/rankService.ts +++ b/api/src/services/db/rankService.ts @@ -1,6 +1,6 @@ import { BatchPromotion, BatchPromotionMember } from "@app/shared/schemas/promotionSchema"; import { PromotionDetails, PromotionSummary } from "@app/shared/types/rank" -import pool from "../db"; +import pool from "../../db"; import { PagedData } from "@app/shared/types/pagination"; import { toDateTime } from "@app/shared/utils/time"; @@ -39,7 +39,6 @@ export async function insertMemberRank(member_id: number, rank_id: number, date? export async function batchInsertMemberRank(promos: BatchPromotionMember[], author: number) { try { var con = await pool.getConnection(); - console.log(promos); promos.forEach(p => { con.query(`CALL sp_update_member_rank(?, ?, ?, ?, ?, ?)`, [p.member_id, p.rank_id, author, author, "Rank Change", toDateTime(new Date(p.start_date))]) }); @@ -70,7 +69,7 @@ export async function getPromotionHistorySummary(page: number = 1, pageSize: num let promoList: PromotionSummary[] = await pool.query(sql, [pageSize, offset]) as PromotionSummary[]; - let loaCount = Number((await pool.query(`SELECT + let rowCount = Number((await pool.query(`SELECT COUNT(*) AS total_grouped_days_count FROM ( @@ -79,10 +78,9 @@ export async function getPromotionHistorySummary(page: number = 1, pageSize: num WHERE reason = 'Rank Change' ) AS grouped_days;`))[0]); - console.log(loaCount); - let pageCount = loaCount / pageSize; + let pageCount = rowCount / pageSize; - let output: PagedData = { data: promoList, pagination: { page: page, pageSize: pageSize, total: loaCount, totalPages: pageCount } } + let output: PagedData = { data: promoList, pagination: { page: page, pageSize: pageSize, total: rowCount, totalPages: pageCount } } return output; } diff --git a/api/src/services/rolesService.ts b/api/src/services/db/rolesService.ts similarity index 98% rename from api/src/services/rolesService.ts rename to api/src/services/db/rolesService.ts index 19eb228..7a857ec 100644 --- a/api/src/services/rolesService.ts +++ b/api/src/services/db/rolesService.ts @@ -1,5 +1,5 @@ import { MemberLight } from '@app/shared/types/member'; -import pool from '../db'; +import pool from '../../db'; import { Role, RoleSummary } from '@app/shared/types/roles' export async function assignUserGroup(userID: number, roleID: number) { diff --git a/api/src/services/statusService.ts b/api/src/services/db/statusService.ts similarity index 89% rename from api/src/services/statusService.ts rename to api/src/services/db/statusService.ts index 8b8fb28..c02f9e4 100644 --- a/api/src/services/statusService.ts +++ b/api/src/services/db/statusService.ts @@ -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())` diff --git a/api/src/services/logging/logger.ts b/api/src/services/logging/logger.ts new file mode 100644 index 0000000..cc4c39e --- /dev/null +++ b/api/src/services/logging/logger.ts @@ -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; // type-specific rich data +} + +// Environment defaults +const CURRENT_DEPTH: LogDepth = (process.env.LOG_DEPTH as LogDepth) || 'normal'; + +const DEPTH_ORDER: Record = { 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, depth: LogDepth = 'normal', context?: Partial) { + 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, depth: LogDepth = 'normal', context?: Partial) { + this.log('info', type, message, data, depth, context); + }, + + debug(type: LogType, message: string, data?: Record, depth: LogDepth = 'normal', context?: Partial) { + this.log('debug', type, message, data, depth, context); + }, + + warn(type: LogType, message: string, data?: Record, depth: LogDepth = 'normal', context?: Partial) { + this.log('warn', type, message, data, depth, context); + }, + + error(type: LogType, message: string, data?: Record, depth: LogDepth = 'normal', context?: Partial) { + this.log('error', type, message, data, depth, context); + }, +} \ No newline at end of file diff --git a/api/tsconfig.json b/api/tsconfig.json index 8893066..d4ab451 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -7,6 +7,7 @@ "node", "express" ], + "sourceMap": true, "paths": { "@app/shared/*": ["../shared/*"] } diff --git a/ui/src/api/rank.ts b/ui/src/api/rank.ts index e391a83..5749ea1 100644 --- a/ui/src/api/rank.ts +++ b/ui/src/api/rank.ts @@ -61,7 +61,6 @@ export async function getPromoHistory(page?: number, pageSize?: number): Promise } export async function getPromotionsOnDay(day: Date): Promise { - console.log(day.toISOString()); const res = await fetch(`${addr}/memberRanks/${day.toISOString()}`, { credentials: 'include', }) diff --git a/ui/src/stores/memberDirectory.ts b/ui/src/stores/memberDirectory.ts index 2f5e542..e01410f 100644 --- a/ui/src/stores/memberDirectory.ts +++ b/ui/src/stores/memberDirectory.ts @@ -105,7 +105,6 @@ export const useMemberDirectory = defineStore('memberDirectory', () => { try { const res = await getFullMembers(ids); for (const m of res) { - console.log(m) full[m.member.member_id] = m; const waiters = fullWaiters.get(m.member.member_id);