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