From 0cc327a9c47664770c0b05aedf828ffa911b31c0 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 8 Mar 2026 10:34:29 -0400 Subject: [PATCH] Added cache busting option for devs --- api/src/routes/members.ts | 28 ++++++++++++- api/src/services/cache/cache.ts | 10 +++++ shared/types/member.ts | 6 +++ ui/src/api/member.ts | 15 ++++++- ui/src/components/Navigation/Navbar.vue | 6 +++ ui/src/pages/DeveloperTools.vue | 52 +++++++++++++++++++++++++ ui/src/router/index.ts | 2 + 7 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 ui/src/pages/DeveloperTools.vue diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index 9bb6933..aaa4d60 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -7,7 +7,7 @@ import { requireLogin, requireMemberState, requireRole } from '../middleware/aut import { getUserActiveLOA } from '../services/db/loaService'; import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers, setUserState, getLastNonSuspendedState } from '../services/db/memberService'; import { getUserRoles, stripUserRoles } from '../services/db/rolesService'; -import { memberSettings, MemberState, myData } from '@app/shared/types/member'; +import { memberSettings, MemberState, myData, UserCacheBustResult } from '@app/shared/types/member'; import { Discharge } from '@app/shared/schemas/dischargeSchema'; import { Performance } from 'perf_hooks'; @@ -211,6 +211,32 @@ router.post('/full/bulk', async (req: Request, res: Response) => { } }) +router.post('/cache/user/bust', [requireLogin, requireMemberState(MemberState.Member), requireRole('dev')], async (req: Request, res: Response) => { + try { + const clearedEntries = memberCache.Clear(); + const payload: UserCacheBustResult = { + success: true, + clearedEntries, + bustedAt: new Date().toISOString(), + }; + + logger.info('app', 'User cache manually busted', { + actor: req.user.id, + clearedEntries, + }); + + return res.status(200).json(payload); + } catch (error) { + logger.error('app', 'Failed to bust user cache', { + caller: req.user?.id, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + + return res.status(500).json({ error: 'Failed to bust user cache' }); + } +}) + router.get('/:id', [requireLogin], async (req, res) => { const userId = req.params.id; diff --git a/api/src/services/cache/cache.ts b/api/src/services/cache/cache.ts index 22c6fb8..7950717 100644 --- a/api/src/services/cache/cache.ts +++ b/api/src/services/cache/cache.ts @@ -16,4 +16,14 @@ export class CacheService { public Invalidate(key: Key): boolean { return this.cacheMap.delete(key); } + + public Size(): number { + return this.cacheMap.size; + } + + public Clear(): number { + const priorSize = this.cacheMap.size; + this.cacheMap.clear(); + return priorSize; + } } \ No newline at end of file diff --git a/shared/types/member.ts b/shared/types/member.ts index 233968d..6ad127a 100644 --- a/shared/types/member.ts +++ b/shared/types/member.ts @@ -50,4 +50,10 @@ export interface myData { LOAs: LOARequest[]; roles: Role[]; state: MemberState; +} + +export interface UserCacheBustResult { + success: boolean; + clearedEntries: number; + bustedAt: string; } \ No newline at end of file diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index 11e9bc0..a96fb95 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,5 +1,5 @@ import { Discharge } from "@shared/schemas/dischargeSchema"; -import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers, MemberState } from "@shared/types/member"; +import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers, MemberState, UserCacheBustResult } from "@shared/types/member"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -157,4 +157,17 @@ export async function unsuspendMember(memberID: number): Promise { throw new Error("Failed to discharge member"); } return true; +} + +export async function bustUserCache(): Promise { + const response = await fetch(`${addr}/members/cache/user/bust`, { + credentials: 'include', + method: 'POST', + }); + + if (!response.ok) { + throw new Error('Failed to bust user cache'); + } + + return response.json(); } \ No newline at end of file diff --git a/ui/src/components/Navigation/Navbar.vue b/ui/src/components/Navigation/Navbar.vue index 8c83549..fcb16fa 100644 --- a/ui/src/components/Navigation/Navbar.vue +++ b/ui/src/components/Navigation/Navbar.vue @@ -153,6 +153,12 @@ function blurAfter() { + + + + Developer + + diff --git a/ui/src/pages/DeveloperTools.vue b/ui/src/pages/DeveloperTools.vue new file mode 100644 index 0000000..548e678 --- /dev/null +++ b/ui/src/pages/DeveloperTools.vue @@ -0,0 +1,52 @@ + + + diff --git a/ui/src/router/index.ts b/ui/src/router/index.ts index b8aec1c..eed6128 100644 --- a/ui/src/router/index.ts +++ b/ui/src/router/index.ts @@ -28,6 +28,8 @@ const router = createRouter({ { path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport/:id', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, + { path: '/developer', component: () => import('@/pages/DeveloperTools.vue'), meta: { requiresAuth: true, memberOnly: true, roles: ['Dev'] } }, + // ADMIN / STAFF ROUTES { path: '/administration', -- 2.37.3.windows.1