Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4ff7d4686 | |||
| 171e3b3137 | |||
| dd2ac19e4a | |||
| 18ba9b576c | |||
| 6c85677e1b | |||
| adb5e3e137 | |||
| 1749c3e617 | |||
| 52ee36be44 | |||
| 0cc327a9c4 | |||
| ef3cbbf370 | |||
| 209d0cdf0f |
@@ -396,11 +396,11 @@ VALUES(?, ?, ?, 1);`
|
|||||||
INNER JOIN members AS member ON member.id = app.poster_id
|
INNER JOIN members AS member ON member.id = app.poster_id
|
||||||
WHERE app.id = ?; `;
|
WHERE app.id = ?; `;
|
||||||
const comment = await conn.query(getSQL, [result.insertId])
|
const comment = await conn.query(getSQL, [result.insertId])
|
||||||
audit.record('application', 'comment_added', { actorId: user.id, targetId: appID }, { commentId: result.insertId });
|
audit.record('application', 'comment_added', { actorId: user.id, targetId: appID }, { commentId: Number(result.insertId) });
|
||||||
logger.info('app', "Admin application comment posted", {
|
logger.info('app', "Admin application comment posted", {
|
||||||
application: appID,
|
application: appID,
|
||||||
poster: user.id,
|
poster: user.id,
|
||||||
comment: result.insertId,
|
comment: Number(result.insertId),
|
||||||
})
|
})
|
||||||
|
|
||||||
res.status(201).json(comment[0]);
|
res.status(201).json(comment[0]);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { requireLogin, requireMemberState, requireRole } from '../middleware/aut
|
|||||||
import { getUserActiveLOA } from '../services/db/loaService';
|
import { getUserActiveLOA } from '../services/db/loaService';
|
||||||
import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers, setUserState, getLastNonSuspendedState } from '../services/db/memberService';
|
import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers, setUserState, getLastNonSuspendedState } from '../services/db/memberService';
|
||||||
import { getUserRoles, stripUserRoles } from '../services/db/rolesService';
|
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 { Discharge } from '@app/shared/schemas/dischargeSchema';
|
||||||
|
|
||||||
import { Performance } from 'perf_hooks';
|
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) => {
|
router.get('/:id', [requireLogin], async (req, res) => {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
|
|
||||||
|
|||||||
10
api/src/services/cache/cache.ts
vendored
10
api/src/services/cache/cache.ts
vendored
@@ -16,4 +16,14 @@ export class CacheService<Key, Value> {
|
|||||||
public Invalidate(key: Key): boolean {
|
public Invalidate(key: Key): boolean {
|
||||||
return this.cacheMap.delete(key);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,8 @@ export async function getFilteredMembers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
whereClauses.push(`v.member_name LIKE ?`);
|
whereClauses.push(`(v.member_name LIKE ? OR v.displayName LIKE ?)`);
|
||||||
|
params.push(`%${search}%`);
|
||||||
params.push(`%${search}%`);
|
params.push(`%${search}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,9 @@ export interface myData {
|
|||||||
roles: Role[];
|
roles: Role[];
|
||||||
state: MemberState;
|
state: MemberState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserCacheBustResult {
|
||||||
|
success: boolean;
|
||||||
|
clearedEntries: number;
|
||||||
|
bustedAt: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Discharge } from "@shared/schemas/dischargeSchema";
|
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
|
// @ts-ignore
|
||||||
const addr = import.meta.env.VITE_APIHOST;
|
const addr = import.meta.env.VITE_APIHOST;
|
||||||
@@ -158,3 +158,16 @@ export async function unsuspendMember(memberID: number): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function bustUserCache(): Promise<UserCacheBustResult> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -153,6 +153,12 @@ function blurAfter() {
|
|||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem v-if="auth.hasRole('Dev')">
|
||||||
|
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
|
||||||
|
<RouterLink to="/developer" @click="blurAfter">Developer</RouterLink>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -227,6 +227,9 @@ onMounted(() => {
|
|||||||
/* Ensure the calendar fills the container properly */
|
/* Ensure the calendar fills the container properly */
|
||||||
:global(.fc) {
|
:global(.fc) {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
--fc-page-bg-color: transparent;
|
||||||
|
--fc-neutral-bg-color: color-mix(in srgb, var(--color-foreground) 8%, transparent);
|
||||||
|
--fc-neutral-text-color: var(--color-muted-foreground);
|
||||||
--fc-border-color: var(--color-border);
|
--fc-border-color: var(--color-border);
|
||||||
--fc-button-bg-color: transparent;
|
--fc-button-bg-color: transparent;
|
||||||
--fc-button-border-color: var(--color-border);
|
--fc-button-border-color: var(--color-border);
|
||||||
@@ -299,6 +302,7 @@ onMounted(() => {
|
|||||||
:global(.fc .fc-scrollgrid td),
|
:global(.fc .fc-scrollgrid td),
|
||||||
:global(.fc .fc-scrollgrid th) {
|
:global(.fc .fc-scrollgrid th) {
|
||||||
border-color: var(--color-border);
|
border-color: var(--color-border);
|
||||||
|
background: var(--fc-page-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Built-in toolbar (if you keep it) ---------- */
|
/* ---------- Built-in toolbar (if you keep it) ---------- */
|
||||||
@@ -346,9 +350,7 @@ onMounted(() => {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(#app > div > div.flex-1.min-h-0 > div > div > div > div.fc.fc-media-screen.fc-direction-ltr.fc-theme-standard > div.fc-view-harness.fc-view-harness-passive > div > table > thead > tr > th) {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
:global(.fc .fc-daygrid-day-top) {
|
:global(.fc .fc-daygrid-day-top) {
|
||||||
padding: 8px 8px 0 8px;
|
padding: 8px 8px 0 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
52
ui/src/pages/DeveloperTools.vue
Normal file
52
ui/src/pages/DeveloperTools.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Button from '@/components/ui/button/Button.vue';
|
||||||
|
import { bustUserCache } from '@/api/member';
|
||||||
|
import type { UserCacheBustResult } from '@shared/types/member';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const result = ref<UserCacheBustResult | null>(null);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
async function onBustUserCache() {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result.value = await bustUserCache();
|
||||||
|
} catch (err) {
|
||||||
|
result.value = null;
|
||||||
|
error.value = err instanceof Error ? err.message : 'Failed to bust user cache';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="max-w-3xl mx-auto pt-10 px-4">
|
||||||
|
<h1 class="scroll-m-20 text-2xl font-semibold tracking-tight">Developer Tools</h1>
|
||||||
|
<p class="mt-2 text-sm text-muted-foreground">
|
||||||
|
Use this page to recover from stale in-memory authentication data after manual database changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-6 rounded-lg border p-5 bg-card">
|
||||||
|
<p class="font-medium">Server User Cache</p>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">
|
||||||
|
This clears the API server's cached user session data so the next request reloads from the database.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center gap-3">
|
||||||
|
<Button :disabled="loading" @click="onBustUserCache">
|
||||||
|
{{ loading ? 'Busting Cache...' : 'Bust User Cache' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="result" class="mt-4 text-sm text-green-700">
|
||||||
|
Cache busted successfully. Cleared {{ result.clearedEntries }} entr{{ result.clearedEntries === 1 ? 'y' : 'ies' }} at
|
||||||
|
{{ new Date(result.bustedAt).toLocaleString() }}.
|
||||||
|
</p>
|
||||||
|
<p v-if="error" class="mt-4 text-sm text-red-700">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -28,10 +28,12 @@ const router = createRouter({
|
|||||||
{ path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
|
{ 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: '/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
|
// ADMIN / STAFF ROUTES
|
||||||
{
|
{
|
||||||
path: '/administration',
|
path: '/administration',
|
||||||
meta: { requiresAuth: true, memberOnly: true, roles: ['17th Administrator', '17th HQ', '17th Command'] },
|
meta: { requiresAuth: true, memberOnly: true, roles: ['17th Administrator', '17th HQ', '17th Command', '17th Recruiter'] },
|
||||||
children: [
|
children: [
|
||||||
{ path: 'applications', component: () => import('@/pages/ManageApplications.vue') },
|
{ path: 'applications', component: () => import('@/pages/ManageApplications.vue') },
|
||||||
{ path: 'applications/:id', component: () => import('@/pages/ManageApplications.vue') },
|
{ path: 'applications/:id', component: () => import('@/pages/ManageApplications.vue') },
|
||||||
|
|||||||
Reference in New Issue
Block a user