diff --git a/src/config.ts b/src/config.ts index e6b1a6f..57e1279 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,7 +29,7 @@ export const AUTH_CONFIG = { // Access Token (JWT in cookie) ACCESS_TOKEN_EXPIRY: "15m" as const, // 15 minutes (short-lived) - ACCESS_TOKEN_EXPIRY_DEV: "2m" as const, // 1 hour in dev for convenience + ACCESS_TOKEN_EXPIRY_DEV: "3m" as const, // 3 minutes for testing // Refresh Token (opaque token in separate cookie) REFRESH_TOKEN_EXPIRY_SHORT: "7d" as const, // 7 days (no remember me) diff --git a/src/context/auth.tsx b/src/context/auth.tsx index 3efa38c..22d6b91 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -8,9 +8,17 @@ * - Never trust client-side state for authorization decisions */ -import { createContext, useContext, Accessor, ParentComponent } from "solid-js"; -import { createAsync } from "@solidjs/router"; -import { getUserState, revalidateAuth, type UserState } from "~/lib/auth-query"; +import { + createContext, + useContext, + createSignal, + onMount, + onCleanup, + Accessor, + ParentComponent +} from "solid-js"; +import { createAsync, revalidate } from "@solidjs/router"; +import { getUserState, type UserState } from "~/lib/auth-query"; interface AuthContextType { /** Current user state (for UI display) */ @@ -41,8 +49,39 @@ interface AuthContextType { const AuthContext = createContext(); export const AuthProvider: ParentComponent = (props) => { - // Get server state via SolidStart query - const serverAuth = createAsync(() => getUserState()); + // Signal to force re-fetch when auth state changes + const [refreshTrigger, setRefreshTrigger] = createSignal(0); + + // Get server state via SolidStart query - tracks refreshTrigger for reactivity + const serverAuth = createAsync( + () => { + refreshTrigger(); // Track the signal to force re-run + return getUserState(); + }, + { deferStream: true } + ); + + // Refresh callback that invalidates cache and forces re-fetch + const refreshAuth = () => { + revalidate(getUserState.key); + setRefreshTrigger((prev) => prev + 1); // Trigger re-fetch + }; + + // Listen for auth refresh events from external sources (token refresh, etc.) + onMount(() => { + if (typeof window === "undefined") return; + + const handleAuthRefresh = () => { + console.log("[AuthContext] Received auth refresh event"); + refreshAuth(); + }; + + window.addEventListener("auth-state-changed", handleAuthRefresh); + + onCleanup(() => { + window.removeEventListener("auth-state-changed", handleAuthRefresh); + }); + }); // Convenience accessors with safe defaults const isAuthenticated = () => serverAuth()?.isAuthenticated ?? false; @@ -60,7 +99,7 @@ export const AuthProvider: ParentComponent = (props) => { userId, isAdmin, isEmailVerified, - refreshAuth: revalidateAuth + refreshAuth }; return ( diff --git a/src/lib/auth-query.ts b/src/lib/auth-query.ts index f24220d..e3f3d96 100644 --- a/src/lib/auth-query.ts +++ b/src/lib/auth-query.ts @@ -26,8 +26,8 @@ export interface UserState { */ export const getUserState = query(async (): Promise => { "use server"; - const { getPrivilegeLevel, getUserID, ConnectionFactory } = - await import("~/server/utils"); + const { getPrivilegeLevel, getUserID } = await import("~/server/auth"); + const { ConnectionFactory } = await import("~/server/utils"); const event = getRequestEvent()!; const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); const userId = await getUserID(event.nativeEvent); @@ -78,4 +78,9 @@ export const getUserState = query(async (): Promise => { */ export function revalidateAuth() { revalidateKey(getUserState.key); + + // Dispatch browser event to trigger UI updates (client-side only) + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("auth-state-changed")); + } } diff --git a/src/lib/token-refresh.ts b/src/lib/token-refresh.ts index bdb1850..467ca28 100644 --- a/src/lib/token-refresh.ts +++ b/src/lib/token-refresh.ts @@ -145,11 +145,3 @@ class TokenRefreshManager { // Singleton instance export const tokenRefreshManager = new TokenRefreshManager(); - -/** - * Manually trigger token refresh (can be called from UI) - * @returns Promise success status - */ -export async function manualRefresh(): Promise { - return tokenRefreshManager.refreshNow(); -} diff --git a/src/routes/account.tsx b/src/routes/account.tsx index 5eafbcd..2dff9fe 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -1,7 +1,6 @@ import { createSignal, Show, createEffect } from "solid-js"; import { PageHead } from "~/components/PageHead"; import { useNavigate, redirect, query, createAsync } from "@solidjs/router"; -import { getEvent } from "vinxi/http"; import XCircle from "~/components/icons/XCircle"; import GoogleLogo from "~/components/icons/GoogleLogo"; import GitHub from "~/components/icons/GitHub"; @@ -22,14 +21,16 @@ import PasswordStrengthMeter from "~/components/PasswordStrengthMeter"; const getUserProfile = query(async (): Promise => { "use server"; - const { getUserID, ConnectionFactory } = await import("~/server/utils"); - const event = getEvent()!; + const { getUserState } = await import("~/lib/auth-query"); + const { ConnectionFactory } = await import("~/server/utils"); - const userId = await getUserID(event); - if (!userId) { + const userState = await getUserState(); + if (!userState.isAuthenticated || !userState.userId) { throw redirect("/login"); } + const userId = userState.userId; + const conn = ConnectionFactory(); try { const res = await conn.execute({ diff --git a/src/routes/blog/[title]/index.tsx b/src/routes/blog/[title]/index.tsx index 6d120b2..0dd38db 100644 --- a/src/routes/blog/[title]/index.tsx +++ b/src/routes/blog/[title]/index.tsx @@ -27,14 +27,15 @@ const getPostByTitle = query( sortBy: "newest" | "oldest" | "highest_rated" | "hot" = "newest" ) => { "use server"; - const { ConnectionFactory, getUserID, getPrivilegeLevel } = - await import("~/server/utils"); + const { getUserState } = await import("~/lib/auth-query"); + const { ConnectionFactory } = await import("~/server/utils"); const { parseConditionals, getSafeEnvVariables } = await import("~/server/conditional-parser"); const { getFeatureFlags } = await import("~/server/feature-flags"); const event = getRequestEvent()!; - const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); - const userID = await getUserID(event.nativeEvent); + const userState = await getUserState(); + const privilegeLevel = userState.privilegeLevel; + const userID = userState.userId; const conn = ConnectionFactory(); if (title === "by-id") { diff --git a/src/routes/blog/index.tsx b/src/routes/blog/index.tsx index 72dcf12..797884e 100644 --- a/src/routes/blog/index.tsx +++ b/src/routes/blog/index.tsx @@ -2,7 +2,6 @@ import { Show } from "solid-js"; import { useSearchParams, A, query } from "@solidjs/router"; import { PageHead } from "~/components/PageHead"; import { createAsync } from "@solidjs/router"; -import { getRequestEvent } from "solid-js/web"; import PostSortingSelect from "~/components/blog/PostSortingSelect"; import TagSelector from "~/components/blog/TagSelector"; import PostSorting from "~/components/blog/PostSorting"; @@ -12,11 +11,11 @@ import { CACHE_CONFIG } from "~/config"; const getPosts = query(async () => { "use server"; - const { ConnectionFactory, getPrivilegeLevel } = - await import("~/server/utils"); + const { getUserState } = await import("~/lib/auth-query"); + const { ConnectionFactory } = await import("~/server/utils"); const { withCache } = await import("~/server/cache"); - const event = getRequestEvent()!; - const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); + const userState = await getUserState(); + const privilegeLevel = userState.privilegeLevel; return withCache( `posts-${privilegeLevel}`, diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 84c70e4..3b583cc 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -1206,7 +1206,7 @@ export const authRouter = createTRPCRouter({ // Apply rate limiting const clientIP = getClientIP(getH3Event(ctx)); - rateLimitRegistration(clientIP, getH3Event(ctx)); + await rateLimitRegistration(clientIP, getH3Event(ctx)); // Schema already validates password match, but double check if (password !== passwordConfirmation) { @@ -1297,7 +1297,7 @@ export const authRouter = createTRPCRouter({ // Apply rate limiting const clientIP = getClientIP(getH3Event(ctx)); - rateLimitLogin(email, clientIP, getH3Event(ctx)); + await rateLimitLogin(email, clientIP, getH3Event(ctx)); const conn = ConnectionFactory(); const res = await conn.execute({ @@ -1602,7 +1602,7 @@ export const authRouter = createTRPCRouter({ // Apply rate limiting const clientIP = getClientIP(getH3Event(ctx)); - rateLimitPasswordReset(clientIP, getH3Event(ctx)); + await rateLimitPasswordReset(clientIP, getH3Event(ctx)); try { const requested = getCookie(getH3Event(ctx), "passwordResetRequested"); @@ -1857,7 +1857,7 @@ export const authRouter = createTRPCRouter({ // Apply rate limiting const clientIP = getClientIP(getH3Event(ctx)); - rateLimitEmailVerification(clientIP, getH3Event(ctx)); + await rateLimitEmailVerification(clientIP, getH3Event(ctx)); try { const requested = getCookie( @@ -2269,7 +2269,7 @@ export const authRouter = createTRPCRouter({ // Admin endpoints for session management cleanupSessions: publicProcedure.mutation(async ({ ctx }) => { // Get user ID to check admin status - const userId = await getUserID(getH3Event(ctx)); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", @@ -2318,7 +2318,7 @@ export const authRouter = createTRPCRouter({ getSessionStats: publicProcedure.query(async ({ ctx }) => { // Get user ID to check admin status - const userId = await getUserID(getH3Event(ctx)); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", diff --git a/src/server/api/routers/post-history.ts b/src/server/api/routers/post-history.ts index b7fd632..aa0a959 100644 --- a/src/server/api/routers/post-history.ts +++ b/src/server/api/routers/post-history.ts @@ -1,7 +1,6 @@ import { createTRPCRouter, publicProcedure } from "../utils"; import { ConnectionFactory } from "~/server/utils"; import { z } from "zod"; -import { getUserID } from "~/server/auth"; import { TRPCError } from "@trpc/server"; import diff from "fast-diff"; @@ -86,7 +85,7 @@ export const postHistoryRouter = createTRPCRouter({ }) ) .mutation(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -168,7 +167,7 @@ export const postHistoryRouter = createTRPCRouter({ getHistory: publicProcedure .input(z.object({ postId: z.number() })) .query(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -243,7 +242,7 @@ export const postHistoryRouter = createTRPCRouter({ restore: publicProcedure .input(z.object({ historyId: z.number() })) .query(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index 04cc515..5671695 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -1,11 +1,6 @@ import { createTRPCRouter, publicProcedure } from "../utils"; import { TRPCError } from "@trpc/server"; -import { - ConnectionFactory, - getUserID, - hashPassword, - checkPassword -} from "~/server/utils"; +import { ConnectionFactory, hashPassword, checkPassword } from "~/server/utils"; import { setCookie } from "vinxi/http"; import type { User } from "~/db/types"; import { toUserProfile } from "~/types/user"; @@ -20,7 +15,7 @@ import { export const userRouter = createTRPCRouter({ getProfile: publicProcedure.query(async ({ ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -49,7 +44,7 @@ export const userRouter = createTRPCRouter({ updateEmail: publicProcedure .input(updateEmailSchema) .mutation(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -83,7 +78,7 @@ export const userRouter = createTRPCRouter({ updateDisplayName: publicProcedure .input(updateDisplayNameSchema) .mutation(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -112,7 +107,7 @@ export const userRouter = createTRPCRouter({ updateProfileImage: publicProcedure .input(updateProfileImageSchema) .mutation(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -141,7 +136,7 @@ export const userRouter = createTRPCRouter({ changePassword: publicProcedure .input(changePasswordSchema) .mutation(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -214,7 +209,7 @@ export const userRouter = createTRPCRouter({ setPassword: publicProcedure .input(setPasswordSchema) .mutation(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ @@ -275,7 +270,7 @@ export const userRouter = createTRPCRouter({ deleteAccount: publicProcedure .input(deleteAccountSchema) .mutation(async ({ input, ctx }) => { - const userId = await getUserID(ctx.event.nativeEvent); + const userId = ctx.userId; if (!userId) { throw new TRPCError({ diff --git a/src/server/security/rate-limit.test.ts b/src/server/security/rate-limit.test.ts index 83e791d..8bc45de 100644 --- a/src/server/security/rate-limit.test.ts +++ b/src/server/security/rate-limit.test.ts @@ -24,44 +24,51 @@ describe("Rate Limiting", () => { }); describe("checkRateLimit", () => { - it("should allow requests within rate limit", () => { + it("should allow requests within rate limit", async () => { const identifier = `test-${Date.now()}`; const maxAttempts = 5; const windowMs = 60000; for (let i = 0; i < maxAttempts; i++) { - const remaining = checkRateLimit(identifier, maxAttempts, windowMs); + const remaining = await checkRateLimit( + identifier, + maxAttempts, + windowMs + ); expect(remaining).toBe(maxAttempts - i - 1); } }); - it("should block requests exceeding rate limit", () => { + it("should block requests exceeding rate limit", async () => { const identifier = `test-${Date.now()}`; const maxAttempts = 3; const windowMs = 60000; // Use up all attempts for (let i = 0; i < maxAttempts; i++) { - checkRateLimit(identifier, maxAttempts, windowMs); + await checkRateLimit(identifier, maxAttempts, windowMs); } // Next attempt should throw - expect(() => { - checkRateLimit(identifier, maxAttempts, windowMs); - }).toThrow(TRPCError); + try { + await checkRateLimit(identifier, maxAttempts, windowMs); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } }); - it("should include remaining time in error message", () => { + it("should include remaining time in error message", async () => { const identifier = `test-${Date.now()}`; const maxAttempts = 2; const windowMs = 60000; // Use up all attempts - checkRateLimit(identifier, maxAttempts, windowMs); - checkRateLimit(identifier, maxAttempts, windowMs); + await checkRateLimit(identifier, maxAttempts, windowMs); + await checkRateLimit(identifier, maxAttempts, windowMs); try { - checkRateLimit(identifier, maxAttempts, windowMs); + await checkRateLimit(identifier, maxAttempts, windowMs); expect.unreachable("Should have thrown TRPCError"); } catch (error) { expect(error).toBeInstanceOf(TRPCError); @@ -74,27 +81,30 @@ describe("Rate Limiting", () => { it("should reset after time window expires", async () => { const identifier = `test-${Date.now()}`; const maxAttempts = 3; - const windowMs = 100; // 100ms window for fast testing + const windowMs = 500; // 500ms window for testing // Use up all attempts for (let i = 0; i < maxAttempts; i++) { - checkRateLimit(identifier, maxAttempts, windowMs); + await checkRateLimit(identifier, maxAttempts, windowMs); } - // Should be blocked - expect(() => { - checkRateLimit(identifier, maxAttempts, windowMs); - }).toThrow(TRPCError); + // Should be blocked immediately after + try { + await checkRateLimit(identifier, maxAttempts, windowMs); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } // Wait for window to expire - await new Promise((resolve) => setTimeout(resolve, 150)); + await new Promise((resolve) => setTimeout(resolve, 600)); // Should be allowed again - const remaining = checkRateLimit(identifier, maxAttempts, windowMs); + const remaining = await checkRateLimit(identifier, maxAttempts, windowMs); expect(remaining).toBe(maxAttempts - 1); }); - it("should handle concurrent requests correctly", () => { + it("should handle concurrent requests correctly", async () => { const identifier = `test-${Date.now()}`; const maxAttempts = 10; const windowMs = 60000; @@ -102,14 +112,14 @@ describe("Rate Limiting", () => { // Simulate concurrent requests const results: number[] = []; for (let i = 0; i < maxAttempts; i++) { - results.push(checkRateLimit(identifier, maxAttempts, windowMs)); + results.push(await checkRateLimit(identifier, maxAttempts, windowMs)); } // All should succeed with decreasing remaining counts expect(results).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); }); - it("should isolate different identifiers", () => { + it("should isolate different identifiers", async () => { const maxAttempts = 3; const windowMs = 60000; @@ -118,16 +128,19 @@ describe("Rate Limiting", () => { // Use up attempts for id1 for (let i = 0; i < maxAttempts; i++) { - checkRateLimit(id1, maxAttempts, windowMs); + await checkRateLimit(id1, maxAttempts, windowMs); } // id1 should be blocked - expect(() => { - checkRateLimit(id1, maxAttempts, windowMs); - }).toThrow(TRPCError); + try { + await checkRateLimit(id1, maxAttempts, windowMs); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } // id2 should still work - const remaining = checkRateLimit(id2, maxAttempts, windowMs); + const remaining = await checkRateLimit(id2, maxAttempts, windowMs); expect(remaining).toBe(maxAttempts - 1); }); }); @@ -191,116 +204,134 @@ describe("Rate Limiting", () => { }); describe("rateLimitLogin", () => { - it("should enforce both IP and email rate limits", () => { + it("should enforce both IP and email rate limits", async () => { const ip = randomIP(); // Should allow up to LOGIN_IP max attempts (5) with different emails // Use different emails to avoid hitting email rate limit for (let i = 0; i < RATE_LIMITS.LOGIN_IP.maxAttempts; i++) { const email = `test-${Date.now()}-${i}@example.com`; - rateLimitLogin(email, ip); + await rateLimitLogin(email, ip); } // Next attempt should fail due to IP limit - expect(() => { + try { const email = `test-${Date.now()}-final@example.com`; - rateLimitLogin(email, ip); - }).toThrow(TRPCError); + await rateLimitLogin(email, ip); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } }); - it("should limit by email independently of IP", () => { + it("should limit by email independently of IP", async () => { const email = `test-${Date.now()}@example.com`; // Use different IPs but same email for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) { - rateLimitLogin(email, randomIP()); + await rateLimitLogin(email, randomIP()); } // Next attempt with different IP should still fail due to email limit - expect(() => { - rateLimitLogin(email, randomIP()); - }).toThrow(TRPCError); + try { + await rateLimitLogin(email, randomIP()); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } }); - it("should allow different emails from same IP within IP limit", () => { + it("should allow different emails from same IP within IP limit", async () => { const ip = randomIP(); // Use different emails but same IP for (let i = 0; i < RATE_LIMITS.LOGIN_IP.maxAttempts; i++) { const email = `test${i}-${Date.now()}@example.com`; - rateLimitLogin(email, ip); + await rateLimitLogin(email, ip); } // Next attempt should fail due to IP limit - expect(() => { - rateLimitLogin(`new-${Date.now()}@example.com`, ip); - }).toThrow(TRPCError); + try { + await rateLimitLogin(`new-${Date.now()}@example.com`, ip); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } }); }); describe("rateLimitPasswordReset", () => { - it("should enforce password reset rate limit", () => { + it("should enforce password reset rate limit", async () => { const ip = randomIP(); // Should allow up to PASSWORD_RESET_IP max attempts (3) for (let i = 0; i < RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts; i++) { - rateLimitPasswordReset(ip); + await rateLimitPasswordReset(ip); } // Next attempt should fail - expect(() => { - rateLimitPasswordReset(ip); - }).toThrow(TRPCError); + try { + await rateLimitPasswordReset(ip); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } }); - it("should isolate password reset limits from login limits", () => { + it("should isolate password reset limits from login limits", async () => { const ip = randomIP(); const email = `test-${Date.now()}@example.com`; // Use up password reset limit for (let i = 0; i < RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts; i++) { - rateLimitPasswordReset(ip); + await rateLimitPasswordReset(ip); } // Should still be able to login (different limit) - rateLimitLogin(email, ip); + await rateLimitLogin(email, ip); }); }); describe("rateLimitRegistration", () => { - it("should enforce registration rate limit", () => { + it("should enforce registration rate limit", async () => { const ip = randomIP(); // Should allow up to REGISTRATION_IP max attempts (3) for (let i = 0; i < RATE_LIMITS.REGISTRATION_IP.maxAttempts; i++) { - rateLimitRegistration(ip); + await rateLimitRegistration(ip); } // Next attempt should fail - expect(() => { - rateLimitRegistration(ip); - }).toThrow(TRPCError); + try { + await rateLimitRegistration(ip); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } }); }); describe("rateLimitEmailVerification", () => { - it("should enforce email verification rate limit", () => { + it("should enforce email verification rate limit", async () => { const ip = randomIP(); // Should allow up to EMAIL_VERIFICATION_IP max attempts (5) for (let i = 0; i < RATE_LIMITS.EMAIL_VERIFICATION_IP.maxAttempts; i++) { - rateLimitEmailVerification(ip); + await rateLimitEmailVerification(ip); } // Next attempt should fail - expect(() => { - rateLimitEmailVerification(ip); - }).toThrow(TRPCError); + try { + await rateLimitEmailVerification(ip); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(TRPCError); + } }); }); describe("Rate Limit Attack Scenarios", () => { - it("should prevent brute force login attacks", () => { + it("should prevent brute force login attacks", async () => { const email = "victim@example.com"; const attackerIP = "1.2.3.4"; @@ -308,7 +339,7 @@ describe("Rate Limiting", () => { let blockedAtAttempt = 0; for (let i = 0; i < 10; i++) { try { - rateLimitLogin(email, attackerIP); + await rateLimitLogin(email, attackerIP); } catch (error) { if (error instanceof TRPCError) { blockedAtAttempt = i; @@ -322,14 +353,14 @@ describe("Rate Limiting", () => { expect(blockedAtAttempt).toBeGreaterThan(0); }); - it("should prevent distributed brute force from multiple IPs", () => { + it("should prevent distributed brute force from multiple IPs", async () => { const email = "victim@example.com"; // Simulate distributed attack from different IPs let blockedAtAttempt = 0; for (let i = 0; i < 10; i++) { try { - rateLimitLogin(email, randomIP()); + await rateLimitLogin(email, randomIP()); } catch (error) { if (error instanceof TRPCError) { blockedAtAttempt = i; @@ -344,14 +375,14 @@ describe("Rate Limiting", () => { ); }); - it("should prevent account enumeration via registration spam", () => { + it("should prevent account enumeration via registration spam", async () => { const attackerIP = randomIP(); // Try to register many accounts to enumerate valid emails let blockedAtAttempt = 0; for (let i = 0; i < 10; i++) { try { - rateLimitRegistration(attackerIP); + await rateLimitRegistration(attackerIP); } catch (error) { if (error instanceof TRPCError) { blockedAtAttempt = i; @@ -364,14 +395,14 @@ describe("Rate Limiting", () => { expect(blockedAtAttempt).toBe(RATE_LIMITS.REGISTRATION_IP.maxAttempts); }); - it("should prevent password reset spam attacks", () => { + it("should prevent password reset spam attacks", async () => { const attackerIP = randomIP(); // Try to spam password resets let blockedAtAttempt = 0; for (let i = 0; i < 10; i++) { try { - rateLimitPasswordReset(attackerIP); + await rateLimitPasswordReset(attackerIP); } catch (error) { if (error instanceof TRPCError) { blockedAtAttempt = i; @@ -415,29 +446,34 @@ describe("Rate Limiting", () => { }); describe("Performance", () => { - it("should handle high volume of rate limit checks efficiently", () => { + it("should handle high volume of rate limit checks efficiently", async () => { const start = performance.now(); - // Check 1000 different identifiers - for (let i = 0; i < 1000; i++) { - checkRateLimit(`test-${i}`, 5, 60000); + // Check 100 different identifiers (reduced from 1000 due to async overhead) + const promises = []; + for (let i = 0; i < 100; i++) { + promises.push(checkRateLimit(`test-${i}`, 5, 60000)); } + await Promise.all(promises); const duration = performance.now() - start; - // Should complete in less than 100ms - expect(duration).toBeLessThan(100); + // Should complete in reasonable time (adjusted for async operations) + expect(duration).toBeLessThan(1000); }); - it("should not leak memory with many identifiers", () => { - // Create many rate limit entries - for (let i = 0; i < 10000; i++) { - checkRateLimit(`test-${i}`, 5, 60000); + it("should not leak memory with many identifiers", async () => { + // Create rate limit entries (reduced significantly due to database overhead) + // Each call performs database operations which are slower than in-memory checks + const promises = []; + for (let i = 0; i < 100; i++) { + promises.push(checkRateLimit(`test-${i}`, 5, 60000)); } + await Promise.all(promises); // This test mainly ensures no crashes occur // Memory cleanup is tested by the cleanup interval in security.ts expect(true).toBe(true); - }); + }, 10000); // Increase timeout to 10 seconds for database operations }); }); diff --git a/src/server/utils.ts b/src/server/utils.ts index deee4c1..eb5b24f 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -1,9 +1,4 @@ -export { - getPrivilegeLevel, - getUserID, - checkAuthStatus, - validateLineageRequest -} from "./auth"; +export { checkAuthStatus, validateLineageRequest } from "./auth"; export { ConnectionFactory,