diff --git a/website/src/app.d.ts b/website/src/app.d.ts index da08e6d..d9fc4c3 100644 --- a/website/src/app.d.ts +++ b/website/src/app.d.ts @@ -1,13 +1,14 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces +import type { User } from '$lib/stores/user-data'; + declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + interface Locals { + userSession: User; + } + interface PageData { + userSession: User; + } + } } export {}; diff --git a/website/src/hooks.server.ts b/website/src/hooks.server.ts index 8d58715..abd3d72 100644 --- a/website/src/hooks.server.ts +++ b/website/src/hooks.server.ts @@ -3,6 +3,10 @@ import { resolveExpiredQuestions, processAccountDeletions } from "$lib/server/jo import { svelteKitHandler } from "better-auth/svelte-kit"; import { redis } from "$lib/server/redis"; import { building } from '$app/environment'; +import { redirect, type Handle } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; async function initializeScheduler() { if (building) return; @@ -68,14 +72,109 @@ async function initializeScheduler() { initializeScheduler(); -export async function handle({ event, resolve }) { - // event.setHeaders({ - // 'Cache-Control': 'private, no-cache, no-store, must-revalidate' - // }); +const sessionCache = new Map(); +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes +const CACHE_CLEANUP_INTERVAL = 10 * 60 * 1000; // 10 minutes + +setInterval(() => { + const now = Date.now(); + for (const [key, value] of sessionCache.entries()) { + if (now - value.timestamp > value.ttl) { + sessionCache.delete(key); + } + } +}, CACHE_CLEANUP_INTERVAL); + +export const handle: Handle = async ({ event, resolve }) => { if (event.url.pathname.startsWith('/.well-known/appspecific/com.chrome.devtools')) { return new Response(null, { status: 204 }); } + // Get session from auth + const session = await auth.api.getSession({ + headers: event.request.headers + }); + + let userData = null; + + if (session?.user) { + const userId = session.user.id; + const cacheKey = `user:${userId}`; + const now = Date.now(); + + const cached = sessionCache.get(cacheKey); + if (cached && (now - cached.timestamp) < cached.ttl) { + userData = cached.userData; + } else { + const [userRecord] = await db + .select({ + id: user.id, + name: user.name, + username: user.username, + email: user.email, + isAdmin: user.isAdmin, + image: user.image, + isBanned: user.isBanned, + banReason: user.banReason, + baseCurrencyBalance: user.baseCurrencyBalance, + bio: user.bio, + volumeMaster: user.volumeMaster, + volumeMuted: user.volumeMuted + }) + .from(user) + .where(eq(user.id, Number(userId))) + .limit(1); + + if (userRecord?.isBanned) { + try { + await auth.api.signOut({ + headers: event.request.headers + }); + } catch (e) { + console.error('Failed to sign out banned user:', e); + } + + if (event.url.pathname !== '/banned') { + const banReason = encodeURIComponent(userRecord.banReason || 'Account suspended'); + throw redirect(302, `/banned?reason=${banReason}`); + } + } else if (userRecord) { + userData = { + id: userRecord.id.toString(), + name: userRecord.name, + username: userRecord.username, + email: userRecord.email, + isAdmin: userRecord.isAdmin || false, + image: userRecord.image || '', + isBanned: userRecord.isBanned || false, + banReason: userRecord.banReason, + avatarUrl: userRecord.image, + baseCurrencyBalance: parseFloat(userRecord.baseCurrencyBalance || '0'), + bio: userRecord.bio || '', + volumeMaster: parseFloat(userRecord.volumeMaster || '0.7'), + volumeMuted: userRecord.volumeMuted || false + }; + + const cacheTTL = userRecord.isAdmin ? CACHE_TTL * 2 : CACHE_TTL; + sessionCache.set(cacheKey, { + userData, + timestamp: now, + ttl: cacheTTL + }); + } + } + } + + event.locals.userSession = userData; + return svelteKitHandler({ event, resolve, auth }); +}; + +export function clearUserCache(userId: string) { + sessionCache.delete(`user:${userId}`); } \ No newline at end of file diff --git a/website/src/lib/components/self/AppSidebar.svelte b/website/src/lib/components/self/AppSidebar.svelte index ffbbfc4..3ed737e 100644 --- a/website/src/lib/components/self/AppSidebar.svelte +++ b/website/src/lib/components/self/AppSidebar.svelte @@ -27,7 +27,10 @@ ChartColumn, TrendingUpDown, Scale, - ShieldCheck + ShieldCheck, + + Hammer + } from 'lucide-svelte'; import { mode, setMode } from 'mode-watcher'; import type { HTMLAttributes } from 'svelte/elements'; @@ -113,6 +116,11 @@ setOpenMobile(false); } + function handleUserManagementClick() { + goto('/admin/users'); + setOpenMobile(false); + } + function handlePromoCodesClick() { goto('/admin/promo'); setOpenMobile(false); @@ -402,6 +410,13 @@ Admin Panel + + + User Management + { : 'private, max-age=30' }); - const sessionResponse = await auth.api.getSession({ - headers: event.request.headers - }); - + // Use the user data already fetched and processed in hooks return { - userSession: sessionResponse?.user || null, + userSession: event.locals.userSession, url: event.url.pathname, }; }; \ No newline at end of file diff --git a/website/src/routes/+layout.svelte b/website/src/routes/+layout.svelte index 18d60d3..42fa970 100644 --- a/website/src/routes/+layout.svelte +++ b/website/src/routes/+layout.svelte @@ -7,7 +7,7 @@ import AppSidebar from '$lib/components/self/AppSidebar.svelte'; import { USER_DATA } from '$lib/stores/user-data'; - import { onMount, onDestroy } from 'svelte'; // onDestroy is already imported + import { onMount } from 'svelte'; import { invalidateAll } from '$app/navigation'; import { ModeWatcher } from 'mode-watcher'; import { page } from '$app/state'; diff --git a/website/src/routes/admin/users/+page.svelte b/website/src/routes/admin/users/+page.svelte new file mode 100644 index 0000000..9be12bf --- /dev/null +++ b/website/src/routes/admin/users/+page.svelte @@ -0,0 +1,201 @@ + + +
+ + + + + Banned Users ({bannedUsers.length}) + + + + + {#if loading} +
+ {#each Array(5) as _} +
+
+ + + +
+ +
+ {/each} +
+ {:else if bannedUsers.length === 0} +
+

No banned users found.

+
+ {:else} +
+ {#each bannedUsers as user} +
+
+
{user.name}
+
@{user.username}
+
+ Reason: {user.banReason} +
+
+ +
+ {/each} +
+ {/if} +
+
+
+ + + + + Ban User + + Enter the username and reason to ban a user. + + +
+
+ + +
+
+ +