diff --git a/src/app.tsx b/src/app.tsx index 52a9aa5..bbd0516 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -153,7 +153,7 @@ function AppLayout(props: { children: any }) { const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => { if (typeof window === "undefined") return; - const currentIsMobile = window.innerWidth < 768; + const currentIsMobile = isMobile(window.innerWidth); // Only hide left bar on mobile when it's visible if (currentIsMobile && leftBarVisible()) { diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index 137e9c3..ee92e4f 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -18,6 +18,7 @@ import { DarkModeToggle } from "./DarkModeToggle"; import { SkeletonBox, SkeletonText } from "./SkeletonLoader"; import { env } from "~/env/client"; import { A, useNavigate, useLocation } from "@solidjs/router"; +import { BREAKPOINTS } from "~/config"; function formatDomainName(url: string): string { const domain = url.split("://")[1]?.split(":")[0] ?? url; @@ -67,7 +68,10 @@ export function RightBarContent() { const [loading, setLoading] = createSignal(true); const handleLinkClick = () => { - if (typeof window !== "undefined" && window.innerWidth < 768) { + if ( + typeof window !== "undefined" && + window.innerWidth < BREAKPOINTS.MOBILE + ) { setLeftBarVisible(false); } }; @@ -210,7 +214,10 @@ export function LeftBar() { const [getLostVisible, setGetLostVisible] = createSignal(false); const handleLinkClick = () => { - if (typeof window !== "undefined" && window.innerWidth < 768) { + if ( + typeof window !== "undefined" && + window.innerWidth < BREAKPOINTS.MOBILE + ) { setLeftBarVisible(false); } }; @@ -311,7 +318,7 @@ export function LeftBar() { if (ref) { // Focus trap for accessibility on mobile const handleKeyDown = (e: KeyboardEvent) => { - const isMobile = window.innerWidth < 768; + const isMobile = window.innerWidth < BREAKPOINTS.MOBILE; if (!isMobile || !leftBarVisible()) return; diff --git a/src/components/Btop.tsx b/src/components/Btop.tsx index a8c1727..7d805a9 100644 --- a/src/components/Btop.tsx +++ b/src/components/Btop.tsx @@ -1,4 +1,5 @@ import { createSignal, onMount, onCleanup, Show } from "solid-js"; +import { BREAKPOINTS } from "~/config"; interface BtopProps { onClose: () => void; @@ -39,10 +40,10 @@ export function Btop(props: BtopProps) { onMount(() => { // Check if mobile if (typeof window !== "undefined") { - setIsMobile(window.innerWidth < 768); + setIsMobile(window.innerWidth < BREAKPOINTS.MOBILE); const handleResize = () => { - setIsMobile(window.innerWidth < 768); + setIsMobile(window.innerWidth < BREAKPOINTS.MOBILE); }; window.addEventListener("resize", handleResize); onCleanup(() => window.removeEventListener("resize", handleResize)); diff --git a/src/components/TerminalErrorPage.tsx b/src/components/TerminalErrorPage.tsx index ca7166f..2af6e06 100644 --- a/src/components/TerminalErrorPage.tsx +++ b/src/components/TerminalErrorPage.tsx @@ -164,14 +164,6 @@ export function TerminalErrorPage(props: TerminalErrorPageProps) { {/* Main content */}
{/* Terminal header */} -
-
- freno@terminal - : - ~ - $ -
-
{/* Error Content - passed as prop */} {props.errorContent} diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index ede4029..6d6363b 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -54,6 +54,7 @@ import ruby from "highlight.js/lib/languages/ruby"; import swift from "highlight.js/lib/languages/swift"; import kotlin from "highlight.js/lib/languages/kotlin"; import dockerfile from "highlight.js/lib/languages/dockerfile"; +import { BREAKPOINTS } from "~/config"; const lowlight = createLowlight(common); @@ -1648,7 +1649,7 @@ export default function TextEditor(props: TextEditorProps) { !hasSuggestion() || !isFullscreen() || typeof window === "undefined" || - window.innerWidth >= 768 + window.innerWidth >= BREAKPOINTS.MOBILE ) { return false; } @@ -1663,7 +1664,7 @@ export default function TextEditor(props: TextEditorProps) { !hasSuggestion() || !isFullscreen() || typeof window === "undefined" || - window.innerWidth >= 768 + window.innerWidth >= BREAKPOINTS.MOBILE ) { return false; } @@ -1737,7 +1738,7 @@ export default function TextEditor(props: TextEditorProps) { if (infillConfig() && !isInitialLoad && infillEnabled()) { const isMobileNotFullscreen = typeof window !== "undefined" && - window.innerWidth < 768 && + window.innerWidth < BREAKPOINTS.MOBILE && !isFullscreen(); // Skip auto-infill on mobile when not in fullscreen @@ -4108,7 +4109,7 @@ export default function TextEditor(props: TextEditorProps) { title={ infillEnabled() ? typeof window !== "undefined" && - window.innerWidth < 768 + window.innerWidth < BREAKPOINTS.MOBILE ? "AI Autocomplete: ON (swipe right to accept full)" : "AI Autocomplete: ON (Ctrl/Cmd+Space to trigger manually)" : "AI Autocomplete: OFF (Click to enable)" diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..adf7e4b --- /dev/null +++ b/src/config.ts @@ -0,0 +1,324 @@ +/** + * Application Configuration + * Central location for all configurable values including timeouts, limits, durations, etc. + */ + +// ============================================================ +// AUTHENTICATION & SESSION +// ============================================================ + +export const AUTH_CONFIG = { + /** JWT token expiration for regular sessions (with remember me) */ + JWT_EXPIRY: "14d" as const, + /** JWT token expiration for sessions without remember me */ + JWT_EXPIRY_SHORT: "12h" as const, + /** Session cookie max age in seconds (14 days) */ + SESSION_COOKIE_MAX_AGE: 60 * 60 * 24 * 14, // 14 days + /** Remember me cookie max age in seconds */ + REMEMBER_ME_MAX_AGE: 60 * 60 * 24 * 14, // 14 days + /** CSRF token cookie max age in seconds (14 days) */ + CSRF_TOKEN_MAX_AGE: 60 * 60 * 24 * 14, // 14 days + /** Lineage JWT expiration for mobile game */ + LINEAGE_JWT_EXPIRY: "14d" as const +} as const; + +// ============================================================ +// RATE LIMITING +// ============================================================ + +export const RATE_LIMITS = { + /** Login: 5 attempts per 15 minutes per IP */ + LOGIN_IP: { maxAttempts: 5, windowMs: 15 * 60 * 1000 }, + /** Login: 3 attempts per hour per email */ + LOGIN_EMAIL: { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, + /** Password reset: 3 attempts per hour per IP */ + PASSWORD_RESET_IP: { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, + /** Registration: 3 attempts per hour per IP */ + REGISTRATION_IP: { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, + /** Email verification: 5 attempts per 15 minutes per IP */ + EMAIL_VERIFICATION_IP: { maxAttempts: 5, windowMs: 15 * 60 * 1000 } +} as const; + +/** Rate limit store cleanup interval (5 minutes) */ +export const RATE_LIMIT_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; + +// ============================================================ +// ACCOUNT SECURITY +// ============================================================ + +export const ACCOUNT_LOCKOUT = { + /** Maximum failed login attempts before account lockout */ + MAX_FAILED_ATTEMPTS: 5, + /** Account lockout duration in milliseconds (5 minutes) */ + LOCKOUT_DURATION_MS: 5 * 60 * 1000 +} as const; + +export const PASSWORD_RESET_CONFIG = { + /** Password reset token expiry (1 hour) */ + TOKEN_EXPIRY_MS: 60 * 60 * 1000 +} as const; + +// ============================================================ +// COOLDOWN TIMERS (CLIENT-SIDE COOKIES) +// ============================================================ + +export const COOLDOWN_TIMERS = { + /** Email login link cooldown (2 minutes) */ + EMAIL_LOGIN_LINK_MS: 2 * 60 * 1000, + /** Email login link cookie max age in seconds */ + EMAIL_LOGIN_LINK_COOKIE_MAX_AGE: 2 * 60, + /** Password reset request cooldown (5 minutes) */ + PASSWORD_RESET_REQUEST_MS: 5 * 60 * 1000, + /** Password reset request cookie max age in seconds */ + PASSWORD_RESET_REQUEST_COOKIE_MAX_AGE: 5 * 60, + /** Contact form request cooldown (1 minute) */ + CONTACT_REQUEST_MS: 1 * 60 * 1000, + /** Contact form request cookie max age in seconds */ + CONTACT_REQUEST_COOKIE_MAX_AGE: 1 * 60, + /** Email verification cooldown (15 minutes) */ + EMAIL_VERIFICATION_MS: 15 * 60 * 1000, + /** Email verification cookie max age in seconds */ + EMAIL_VERIFICATION_COOKIE_MAX_AGE: 15 * 60 +} as const; + +// ============================================================ +// CACHE & DATA PERSISTENCE +// ============================================================ + +export const CACHE_CONFIG = { + /** Blog cache TTL (24 hours) */ + BLOG_CACHE_TTL_MS: 24 * 60 * 60 * 1000, + /** Git activity cache TTL (10 minutes) */ + GIT_ACTIVITY_CACHE_TTL_MS: 10 * 60 * 1000, + /** Blog posts list cache TTL (5 minutes) */ + BLOG_POSTS_LIST_CACHE_TTL_MS: 5 * 60 * 1000, + /** Maximum stale data age (7 days) */ + MAX_STALE_DATA_MS: 7 * 24 * 60 * 60 * 1000, + /** Git activity max stale age (24 hours) */ + GIT_ACTIVITY_MAX_STALE_MS: 24 * 60 * 60 * 1000 +} as const; + +// ============================================================ +// NETWORK & API +// ============================================================ + +export const NETWORK_CONFIG = { + /** Default API timeout for email service (15 seconds) */ + EMAIL_API_TIMEOUT_MS: 15000, + /** Default API timeout for GitHub OAuth (15 seconds) */ + GITHUB_API_TIMEOUT_MS: 15000, + /** Default API timeout for Google OAuth (15 seconds) */ + GOOGLE_API_TIMEOUT_MS: 15000, + /** Maximum retry attempts for failed requests */ + MAX_RETRIES: 2, + /** Retry delay between attempts (1 second) */ + RETRY_DELAY_MS: 1000 +} as const; + +// ============================================================ +// UI/UX - TYPEWRITER COMPONENT +// ============================================================ + +export const TYPEWRITER_CONFIG = { + /** Default typing speed (characters per second) */ + DEFAULT_SPEED: 30, + /** Fast typing speed */ + FAST_SPEED: 80, + /** Slow typing speed */ + SLOW_SPEED: 10, + /** Very slow typing speed */ + VERY_SLOW_SPEED: 100, + /** Extra slow typing speed */ + EXTRA_SLOW_SPEED: 120, + /** Default keep alive duration (ms) */ + DEFAULT_KEEP_ALIVE_MS: 2000, + /** Long keep alive duration (ms) */ + LONG_KEEP_ALIVE_MS: 10000, + /** Default initial delay (ms) */ + DEFAULT_DELAY_MS: 500, + /** Cursor fade delay after completion (1 second) */ + CURSOR_FADE_DELAY_MS: 1000 +} as const; + +// ============================================================ +// UI/UX - COUNTDOWN TIMER COMPONENT +// ============================================================ + +export const COUNTDOWN_CONFIG = { + /** Email login link countdown duration (2 minutes) */ + EMAIL_LOGIN_LINK_DURATION_S: 120, + /** Password reset countdown duration (5 minutes) */ + PASSWORD_RESET_DURATION_S: 300, + /** Contact form countdown duration (1 minute) */ + CONTACT_FORM_DURATION_S: 60, + /** Password reset success redirect countdown (5 seconds) */ + PASSWORD_RESET_SUCCESS_DURATION_S: 5, + /** Default timer size (pixels) */ + DEFAULT_TIMER_SIZE_PX: 48, + /** Large timer size (pixels) */ + LARGE_TIMER_SIZE_PX: 200, + /** Default stroke width */ + DEFAULT_STROKE_WIDTH: 6, + /** Large stroke width */ + LARGE_STROKE_WIDTH: 12 +} as const; + +// ============================================================ +// UI/UX - RESPONSIVE BREAKPOINTS +// ============================================================ + +export const BREAKPOINTS = { + /** Mobile breakpoint (pixels) */ + MOBILE_MAX_WIDTH: 768, + /** Tablet breakpoint (pixels) */ + TABLET_MAX_WIDTH: 1024, + /** Desktop minimum width (pixels) */ + DESKTOP_MIN_WIDTH: 1025 +} as const; + +// ============================================================ +// UI/UX - ANIMATIONS & TRANSITIONS +// ============================================================ + +export const ANIMATION_CONFIG = { + /** Standard transition duration (ms) */ + TRANSITION_DURATION_MS: 300, + /** Fast transition duration (ms) */ + FAST_TRANSITION_MS: 200, + /** Slow transition duration (ms) */ + SLOW_TRANSITION_MS: 500, + /** Extra slow transition duration (ms) */ + EXTRA_SLOW_TRANSITION_MS: 600, + /** Sidebar toggle duration (ms) */ + SIDEBAR_DURATION_MS: 500, + /** Menu typing effect delay (ms) */ + MENU_TYPING_DELAY_MS: 140, + /** Menu initial delay (ms) */ + MENU_INITIAL_DELAY_MS: 500, + /** Success message auto-hide duration (ms) */ + SUCCESS_MESSAGE_DURATION_MS: 3000, + /** Error message auto-hide duration (ms) */ + ERROR_MESSAGE_DURATION_MS: 5000, + /** Redirect delay after successful action (ms) */ + REDIRECT_DELAY_MS: 500 +} as const; + +// ============================================================ +// UI/UX - PDF VIEWER +// ============================================================ + +export const PDF_CONFIG = { + /** PDF rendering scale */ + RENDER_SCALE: 1.5 +} as const; + +// ============================================================ +// UI/UX - 401 ERROR PAGE +// ============================================================ + +export const ERROR_PAGE_CONFIG = { + /** Glitch effect interval (ms) */ + GLITCH_INTERVAL_MS: 300, + /** Glitch effect duration (ms) */ + GLITCH_DURATION_MS: 100, + /** Number of particles for background animation */ + PARTICLE_COUNT: 45 +} as const; + +// ============================================================ +// VALIDATION +// ============================================================ + +export const VALIDATION_CONFIG = { + /** Minimum password length (must match securePasswordSchema in schemas/user.ts) */ + MIN_PASSWORD_LENGTH: 12, + /** Maximum message length for contact form */ + MAX_CONTACT_MESSAGE_LENGTH: 500, + /** Minimum password confirmation match length before showing error */ + MIN_PASSWORD_CONF_LENGTH_FOR_ERROR: 6 +} as const; + +// ============================================================ +// LINEAGE GAME (MOBILE APP) +// ============================================================ + +export const LINEAGE_CONFIG = { + /** Database deletion grace period (24 hours) */ + DELETION_GRACE_PERIOD_MS: 24 * 60 * 60 * 1000, + /** PvP opponents returned per query */ + PVP_OPPONENTS_COUNT: 3 +} as const; + +// ============================================================ +// AUDIT & LOGGING +// ============================================================ + +export const AUDIT_CONFIG = { + /** Default query limit for audit logs */ + DEFAULT_QUERY_LIMIT: 100, + /** Maximum audit log retention (90 days) */ + MAX_RETENTION_DAYS: 90 +} as const; + +// ============================================================ +// HELPER FUNCTIONS +// ============================================================ + +/** + * Convert milliseconds to seconds + */ +export function msToSeconds(ms: number): number { + return Math.floor(ms / 1000); +} + +/** + * Convert seconds to milliseconds + */ +export function secondsToMs(seconds: number): number { + return seconds * 1000; +} + +/** + * Convert minutes to milliseconds + */ +export function minutesToMs(minutes: number): number { + return minutes * 60 * 1000; +} + +/** + * Convert hours to milliseconds + */ +export function hoursToMs(hours: number): number { + return hours * 60 * 60 * 1000; +} + +/** + * Convert days to milliseconds + */ +export function daysToMs(days: number): number { + return days * 24 * 60 * 60 * 1000; +} + +/** + * Check if screen width is mobile + */ +export function isMobileWidth(width: number): boolean { + return width < BREAKPOINTS.MOBILE_MAX_WIDTH; +} + +/** + * Check if screen width is tablet + */ +export function isTabletWidth(width: number): boolean { + return ( + width >= BREAKPOINTS.MOBILE_MAX_WIDTH && + width <= BREAKPOINTS.TABLET_MAX_WIDTH + ); +} + +/** + * Check if screen width is desktop + */ +export function isDesktopWidth(width: number): boolean { + return width >= BREAKPOINTS.DESKTOP_MIN_WIDTH; +} diff --git a/src/lib/validation.ts b/src/lib/validation.ts index c02f3a7..9adbf93 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -2,6 +2,8 @@ * Form validation utilities */ +import { VALIDATION_CONFIG } from "~/config"; + /** * Validate email format */ @@ -36,9 +38,11 @@ export function validatePassword(password: string): { } { const errors: string[] = []; - // Minimum length: 12 characters - if (password.length < 12) { - errors.push("Password must be at least 12 characters long"); + // Minimum length from config + if (password.length < VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) { + errors.push( + `Password must be at least ${VALIDATION_CONFIG.MIN_PASSWORD_LENGTH} characters long` + ); } // Require uppercase letter @@ -93,7 +97,7 @@ export function validatePassword(password: string): { strength = "strong"; } else if (password.length >= 16) { strength = "good"; - } else if (password.length >= 12) { + } else if (password.length >= VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) { strength = "fair"; } } diff --git a/src/routes/401.tsx b/src/routes/401.tsx index da832d7..e815be7 100644 --- a/src/routes/401.tsx +++ b/src/routes/401.tsx @@ -2,6 +2,7 @@ import { Title, Meta } from "@solidjs/meta"; import { HttpStatusCode } from "@solidjs/start"; import { useNavigate } from "@solidjs/router"; import { createEffect, createSignal, For } from "solid-js"; +import { ERROR_PAGE_CONFIG } from "~/config"; export default function Page_401() { const navigate = useNavigate(); @@ -24,15 +25,18 @@ export default function Page_401() { } setGlitchText(glitched); - setTimeout(() => setGlitchText(originalText), 100); + setTimeout( + () => setGlitchText(originalText), + ERROR_PAGE_CONFIG.GLITCH_DURATION_MS + ); } - }, 300); + }, ERROR_PAGE_CONFIG.GLITCH_INTERVAL_MS); return () => clearInterval(glitchInterval); }); const createParticles = () => { - return Array.from({ length: 45 }, (_, i) => ({ + return Array.from({ length: ERROR_PAGE_CONFIG.PARTICLE_COUNT }, (_, i) => ({ id: i, left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`, diff --git a/src/routes/account.tsx b/src/routes/account.tsx index 4ff2c9d..8585592 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -12,6 +12,7 @@ import Dropzone from "~/components/blog/Dropzone"; import AddImageToS3 from "~/lib/s3upload"; import { validatePassword, isValidEmail } from "~/lib/validation"; import { TerminalSplash } from "~/components/TerminalSplash"; +import { VALIDATION_CONFIG } from "~/config"; import type { UserProfile } from "~/types/user"; @@ -806,7 +807,7 @@ export default function AccountPage() { ref={oldPasswordRef} type={showOldPasswordInput() ? "text" : "password"} required - minlength="8" + minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH} disabled={passwordChangeLoading()} placeholder=" " title="Password must be at least 8 characters" @@ -997,7 +998,7 @@ export default function AccountPage() { ref={deleteAccountPasswordRef} type="password" required - minlength="8" + minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH} disabled={deleteAccountButtonLoading()} placeholder=" " title="Enter your password to confirm account deletion" diff --git a/src/routes/blog/index.tsx b/src/routes/blog/index.tsx index 514b248..93f0ef2 100644 --- a/src/routes/blog/index.tsx +++ b/src/routes/blog/index.tsx @@ -8,6 +8,7 @@ import TagSelector from "~/components/blog/TagSelector"; import PostSorting from "~/components/blog/PostSorting"; import PublishStatusToggle from "~/components/blog/PublishStatusToggle"; import { TerminalSplash } from "~/components/TerminalSplash"; +import { CACHE_CONFIG } from "~/config"; const getPosts = query(async () => { "use server"; @@ -17,11 +18,14 @@ const getPosts = query(async () => { const event = getRequestEvent()!; const privilegeLevel = await getPrivilegeLevel(event.nativeEvent); - return withCache(`posts-${privilegeLevel}`, 5 * 60 * 1000, async () => { - const conn = ConnectionFactory(); + return withCache( + `posts-${privilegeLevel}`, + CACHE_CONFIG.BLOG_POSTS_LIST_CACHE_TTL_MS, + async () => { + const conn = ConnectionFactory(); - // Fetch all posts with aggregated data - let postsQuery = ` + // Fetch all posts with aggregated data + let postsQuery = ` SELECT p.id, p.title, @@ -41,17 +45,17 @@ const getPosts = query(async () => { LEFT JOIN Comment c ON p.id = c.post_id `; - if (privilegeLevel !== "admin") { - postsQuery += ` WHERE p.published = TRUE`; - } + if (privilegeLevel !== "admin") { + postsQuery += ` WHERE p.published = TRUE`; + } - postsQuery += ` GROUP BY p.id, p.title, p.subtitle, p.body, p.banner_photo, p.date, p.published, p.category, p.author_id, p.reads, p.attachments`; - postsQuery += ` ORDER BY p.date ASC;`; + postsQuery += ` GROUP BY p.id, p.title, p.subtitle, p.body, p.banner_photo, p.date, p.published, p.category, p.author_id, p.reads, p.attachments`; + postsQuery += ` ORDER BY p.date ASC;`; - const postsResult = await conn.execute(postsQuery); - const posts = postsResult.rows; + const postsResult = await conn.execute(postsQuery); + const posts = postsResult.rows; - const tagsQuery = ` + const tagsQuery = ` SELECT t.value, t.post_id FROM Tag t JOIN Post p ON t.post_id = p.id @@ -59,17 +63,18 @@ const getPosts = query(async () => { ORDER BY t.value ASC `; - const tagsResult = await conn.execute(tagsQuery); - const tags = tagsResult.rows; + const tagsResult = await conn.execute(tagsQuery); + const tags = tagsResult.rows; - const tagMap: Record = {}; - tags.forEach((tag: any) => { - const key = `${tag.value}`; - tagMap[key] = (tagMap[key] || 0) + 1; - }); + const tagMap: Record = {}; + tags.forEach((tag: any) => { + const key = `${tag.value}`; + tagMap[key] = (tagMap[key] || 0) + 1; + }); - return { posts, tags, tagMap, privilegeLevel }; - }); + return { posts, tags, tagMap, privilegeLevel }; + } + ); }, "posts"); export default function BlogIndex() { diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx index d4308e4..b3d994e 100644 --- a/src/routes/contact.tsx +++ b/src/routes/contact.tsx @@ -26,6 +26,12 @@ import { TimeoutError, APIError } from "~/server/fetch-utils"; +import { + NETWORK_CONFIG, + COOLDOWN_TIMERS, + VALIDATION_CONFIG, + COUNTDOWN_CONFIG +} from "~/config"; const getContactData = query(async () => { "use server"; @@ -52,7 +58,7 @@ const sendContactEmail = action(async (formData: FormData) => { message: z .string() .min(1, "Message is required") - .max(500, "Message too long") + .max(VALIDATION_CONFIG.MAX_CONTACT_MESSAGE_LENGTH, "Message too long") }); try { @@ -99,20 +105,20 @@ const sendContactEmail = action(async (formData: FormData) => { "content-type": "application/json" }, body: JSON.stringify(sendinblueData), - timeout: 15000 + timeout: NETWORK_CONFIG.EMAIL_API_TIMEOUT_MS }); await checkResponse(response); return response; }, { - maxRetries: 2, - retryDelay: 1000 + maxRetries: NETWORK_CONFIG.MAX_RETRIES, + retryDelay: NETWORK_CONFIG.RETRY_DELAY_MS } ); // Set cooldown cookie - const exp = new Date(Date.now() + 1 * 60 * 1000); + const exp = new Date(Date.now() + COOLDOWN_TIMERS.CONTACT_REQUEST_MS); setCookie("contactRequestSent", exp.toUTCString(), { expires: exp, path: "/" @@ -418,7 +424,7 @@ export default function ContactPage() { title="Please enter your message" class="underlinedInput w-full bg-transparent" rows={4} - maxlength={500} + maxlength={VALIDATION_CONFIG.MAX_CONTACT_MESSAGE_LENGTH} /> @@ -456,7 +462,7 @@ export default function ContactPage() { } > { "use server"; @@ -325,7 +326,7 @@ export default function LoginPage() { }; const checkPasswordLength = (password: string) => { - if (password.length >= 8) { + if (password.length >= VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) { setPasswordLengthSufficient(true); setShowPasswordLengthWarning(false); } else { @@ -628,7 +629,7 @@ export default function LoginPage() { } > { - if (password.length >= 8) { + if (password.length >= VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) { setPasswordLengthSufficient(true); setShowPasswordLengthWarning(false); } else { @@ -223,7 +224,8 @@ export default function PasswordResetPage() { showPasswordLengthWarning() ? "" : "opacity-0 select-none" } text-red text-center transition-opacity duration-200 ease-in-out`} > - Password too short! Min Length: 8 + Password too short! Min Length:{" "} + {VALIDATION_CONFIG.MIN_PASSWORD_LENGTH}
{/* Password Confirmation Input */} @@ -279,7 +281,8 @@ export default function PasswordResetPage() { !passwordsMatch() && passwordLengthSufficient() && newPasswordConfRef && - newPasswordConfRef.value.length >= 6 + newPasswordConfRef.value.length >= + VALIDATION_CONFIG.MIN_PASSWORD_CONF_LENGTH_FOR_ERROR ? "" : "opacity-0 select-none" } text-red text-center transition-opacity duration-200 ease-in-out`} @@ -307,7 +310,7 @@ export default function PasswordResetPage() {
([]); @@ -26,7 +27,7 @@ export default function Resume() { // Render each page for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { const page = await pdf.getPage(pageNum); - const viewport = page.getViewport({ scale: 1.5 }); + const viewport = page.getViewport({ scale: PDF_CONFIG.RENDER_SCALE }); // Create canvas const canvas = document.createElement("canvas"); diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index f4f583e..e76976c 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -46,6 +46,7 @@ import { import { logAuditEvent } from "~/server/audit"; import type { H3Event } from "vinxi/http"; import type { Context } from "../utils"; +import { AUTH_CONFIG, NETWORK_CONFIG, COOLDOWN_TIMERS } from "~/config"; /** * Safely extract H3Event from Context @@ -70,7 +71,7 @@ function getH3Event(ctx: Context): H3Event { async function createJWT( userId: string, sessionId: string, - expiresIn: string = "14d" + expiresIn: string = AUTH_CONFIG.JWT_EXPIRY ): Promise { const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); const token = await new SignJWT({ @@ -173,15 +174,15 @@ async function sendEmail(to: string, subject: string, htmlContent: string) { "content-type": "application/json" }, body: JSON.stringify(sendinblueData), - timeout: 15000 + timeout: NETWORK_CONFIG.EMAIL_API_TIMEOUT_MS }); await checkResponse(response); return response; }, { - maxRetries: 2, - retryDelay: 1000 + maxRetries: NETWORK_CONFIG.MAX_RETRIES, + retryDelay: NETWORK_CONFIG.RETRY_DELAY_MS } ); } @@ -206,7 +207,7 @@ export const authRouter = createTRPCRouter({ client_secret: env.GITHUB_CLIENT_SECRET, code }), - timeout: 15000 + timeout: NETWORK_CONFIG.GITHUB_API_TIMEOUT_MS } ); @@ -226,7 +227,7 @@ export const authRouter = createTRPCRouter({ headers: { Authorization: `token ${access_token}` }, - timeout: 15000 + timeout: NETWORK_CONFIG.GITHUB_API_TIMEOUT_MS } ); @@ -241,7 +242,7 @@ export const authRouter = createTRPCRouter({ headers: { Authorization: `token ${access_token}` }, - timeout: 15000 + timeout: NETWORK_CONFIG.GITHUB_API_TIMEOUT_MS } ); @@ -319,7 +320,7 @@ export const authRouter = createTRPCRouter({ const userAgent = getUserAgent(getH3Event(ctx)); const sessionId = await createSession( userId, - "14d", + AUTH_CONFIG.JWT_EXPIRY, clientIP, userAgent ); @@ -327,7 +328,7 @@ export const authRouter = createTRPCRouter({ const token = await createJWT(userId, sessionId); setCookie(getH3Event(ctx), "userIDToken", token, { - maxAge: 60 * 60 * 24 * 14, // 14 days + maxAge: AUTH_CONFIG.SESSION_COOKIE_MAX_AGE, path: "/", httpOnly: true, secure: env.NODE_ENV === "production", @@ -417,7 +418,7 @@ export const authRouter = createTRPCRouter({ redirect_uri: `${env.VITE_DOMAIN || "https://freno.me"}/api/auth/callback/google`, grant_type: "authorization_code" }), - timeout: 15000 + timeout: NETWORK_CONFIG.GOOGLE_API_TIMEOUT_MS } ); @@ -437,7 +438,7 @@ export const authRouter = createTRPCRouter({ headers: { Authorization: `Bearer ${access_token}` }, - timeout: 15000 + timeout: NETWORK_CONFIG.GOOGLE_API_TIMEOUT_MS } ); @@ -501,7 +502,7 @@ export const authRouter = createTRPCRouter({ const userAgent = getUserAgent(getH3Event(ctx)); const sessionId = await createSession( userId, - "14d", + AUTH_CONFIG.JWT_EXPIRY, clientIP, userAgent ); @@ -509,7 +510,7 @@ export const authRouter = createTRPCRouter({ const token = await createJWT(userId, sessionId); setCookie(getH3Event(ctx), "userIDToken", token, { - maxAge: 60 * 60 * 24 * 14, // 14 days + maxAge: AUTH_CONFIG.SESSION_COOKIE_MAX_AGE, path: "/", httpOnly: true, secure: env.NODE_ENV === "production", @@ -618,7 +619,9 @@ export const authRouter = createTRPCRouter({ // Create session with client info const clientIP = getClientIP(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx)); - const expiresIn = rememberMe ? "14d" : "12h"; + const expiresIn = rememberMe + ? AUTH_CONFIG.JWT_EXPIRY + : AUTH_CONFIG.JWT_EXPIRY_SHORT; const sessionId = await createSession( userId, expiresIn, @@ -636,7 +639,7 @@ export const authRouter = createTRPCRouter({ }; if (rememberMe) { - cookieOptions.maxAge = 60 * 60 * 24 * 14; + cookieOptions.maxAge = AUTH_CONFIG.REMEMBER_ME_MAX_AGE; } setCookie(getH3Event(ctx), "userIDToken", userToken, cookieOptions); @@ -790,7 +793,7 @@ export const authRouter = createTRPCRouter({ const userAgent = getUserAgent(getH3Event(ctx)); const sessionId = await createSession( userId, - "14d", + AUTH_CONFIG.JWT_EXPIRY, clientIP, userAgent ); @@ -798,7 +801,7 @@ export const authRouter = createTRPCRouter({ const token = await createJWT(userId, sessionId); setCookie(getH3Event(ctx), "userIDToken", token, { - maxAge: 60 * 60 * 24 * 14, // 14 days + maxAge: AUTH_CONFIG.SESSION_COOKIE_MAX_AGE, path: "/", httpOnly: true, secure: env.NODE_ENV === "production", @@ -958,7 +961,9 @@ export const authRouter = createTRPCRouter({ // Reset failed attempts on successful login await resetFailedAttempts(user.id); - const expiresIn = rememberMe ? "14d" : "12h"; + const expiresIn = rememberMe + ? AUTH_CONFIG.JWT_EXPIRY + : AUTH_CONFIG.JWT_EXPIRY_SHORT; // Create session with client info (reuse clientIP from rate limiting) const userAgent = getUserAgent(getH3Event(ctx)); @@ -979,7 +984,7 @@ export const authRouter = createTRPCRouter({ }; if (rememberMe) { - cookieOptions.maxAge = 60 * 60 * 24 * 14; // 14 days + cookieOptions.maxAge = AUTH_CONFIG.REMEMBER_ME_MAX_AGE; } setCookie(getH3Event(ctx), "userIDToken", token, cookieOptions); @@ -1110,13 +1115,13 @@ export const authRouter = createTRPCRouter({ await sendEmail(email, "freno.me login link", htmlContent); - const exp = new Date(Date.now() + 2 * 60 * 1000); + const exp = new Date(Date.now() + COOLDOWN_TIMERS.EMAIL_LOGIN_LINK_MS); setCookie( getH3Event(ctx), "emailLoginLinkRequested", exp.toUTCString(), { - maxAge: 2 * 60, + maxAge: COOLDOWN_TIMERS.EMAIL_LOGIN_LINK_COOKIE_MAX_AGE, path: "/" } ); @@ -1228,13 +1233,15 @@ export const authRouter = createTRPCRouter({ await sendEmail(email, "password reset", htmlContent); - const exp = new Date(Date.now() + 5 * 60 * 1000); + const exp = new Date( + Date.now() + COOLDOWN_TIMERS.PASSWORD_RESET_REQUEST_MS + ); setCookie( getH3Event(ctx), "passwordResetRequested", exp.toUTCString(), { - maxAge: 5 * 60, + maxAge: COOLDOWN_TIMERS.PASSWORD_RESET_REQUEST_COOKIE_MAX_AGE, path: "/" } ); @@ -1417,9 +1424,9 @@ export const authRouter = createTRPCRouter({ if (requested) { const time = parseInt(requested); const currentTime = Date.now(); - const difference = (currentTime - time) / (1000 * 60); + const difference = (currentTime - time) / 1000; - if (difference < 15) { + if (difference * 1000 < COOLDOWN_TIMERS.EMAIL_VERIFICATION_MS) { throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: @@ -1492,7 +1499,7 @@ export const authRouter = createTRPCRouter({ "emailVerificationRequested", Date.now().toString(), { - maxAge: 15 * 60, + maxAge: COOLDOWN_TIMERS.EMAIL_VERIFICATION_COOKIE_MAX_AGE, path: "/" } ); diff --git a/src/server/api/routers/blog.ts b/src/server/api/routers/blog.ts index 6bbe142..353b7bd 100644 --- a/src/server/api/routers/blog.ts +++ b/src/server/api/routers/blog.ts @@ -3,8 +3,9 @@ import { ConnectionFactory } from "~/server/utils"; import { withCacheAndStale } from "~/server/cache"; import { incrementPostReadSchema } from "../schemas/blog"; import type { PostWithCommentsAndLikes } from "~/db/types"; +import { CACHE_CONFIG } from "~/config"; -const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours +const BLOG_CACHE_TTL = CACHE_CONFIG.BLOG_CACHE_TTL_MS; // Shared cache function for all blog posts const getAllPostsData = async (privilegeLevel: string) => { diff --git a/src/server/api/routers/database.ts b/src/server/api/routers/database.ts index 6ee2442..e143e73 100644 --- a/src/server/api/routers/database.ts +++ b/src/server/api/routers/database.ts @@ -31,8 +31,9 @@ import { updateUserImageSchema, updateUserEmailSchema } from "../schemas/database"; +import { CACHE_CONFIG } from "~/config"; -const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours +const BLOG_CACHE_TTL = CACHE_CONFIG.BLOG_CACHE_TTL_MS; export const databaseRouter = createTRPCRouter({ getCommentReactions: publicProcedure diff --git a/src/server/api/routers/git-activity.ts b/src/server/api/routers/git-activity.ts index 507da13..63ecd3e 100644 --- a/src/server/api/routers/git-activity.ts +++ b/src/server/api/routers/git-activity.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "../utils"; import { env } from "~/env/server"; import { withCacheAndStale } from "~/server/cache"; +import { CACHE_CONFIG } from "~/config"; import { fetchWithTimeout, checkResponse, @@ -30,7 +31,7 @@ export const gitActivityRouter = createTRPCRouter({ .query(async ({ input }) => { return withCacheAndStale( `github-commits-${input.limit}`, - 10 * 60 * 1000, // 10 minutes + CACHE_CONFIG.GIT_ACTIVITY_CACHE_TTL_MS, async () => { const reposResponse = await fetchWithTimeout( `https://api.github.com/users/MikeFreno/repos?sort=pushed&per_page=10`, @@ -108,7 +109,7 @@ export const gitActivityRouter = createTRPCRouter({ return allCommits.slice(0, input.limit); }, - { maxStaleMs: 24 * 60 * 60 * 1000 } // Accept stale data up to 24 hours old + { maxStaleMs: CACHE_CONFIG.GIT_ACTIVITY_MAX_STALE_MS } ).catch((error) => { if (error instanceof NetworkError) { console.error("GitHub API unavailable (network error)"); @@ -130,7 +131,7 @@ export const gitActivityRouter = createTRPCRouter({ .query(async ({ input }) => { return withCacheAndStale( `gitea-commits-${input.limit}`, - 10 * 60 * 1000, // 10 minutes + CACHE_CONFIG.GIT_ACTIVITY_CACHE_TTL_MS, async () => { const reposResponse = await fetchWithTimeout( `${env.GITEA_URL}/api/v1/users/Mike/repos?limit=100`, @@ -210,7 +211,7 @@ export const gitActivityRouter = createTRPCRouter({ return allCommits.slice(0, input.limit); }, - { maxStaleMs: 24 * 60 * 60 * 1000 } + { maxStaleMs: CACHE_CONFIG.GIT_ACTIVITY_MAX_STALE_MS } ).catch((error) => { if (error instanceof NetworkError) { console.error("Gitea API unavailable (network error)"); diff --git a/src/server/api/routers/misc.ts b/src/server/api/routers/misc.ts index 204c8d4..9473bee 100644 --- a/src/server/api/routers/misc.ts +++ b/src/server/api/routers/misc.ts @@ -20,6 +20,7 @@ import { TimeoutError, APIError } from "~/server/fetch-utils"; +import { NETWORK_CONFIG, COOLDOWN_TIMERS, VALIDATION_CONFIG } from "~/config"; const assets: Record = { "shapes-with-abigail": "shapes-with-abigail.apk", "magic-delve": "magic-delve.apk", @@ -257,7 +258,10 @@ export const miscRouter = createTRPCRouter({ z.object({ name: z.string().min(1), email: z.string().email(), - message: z.string().min(1).max(500) + message: z + .string() + .min(1) + .max(VALIDATION_CONFIG.MAX_CONTACT_MESSAGE_LENGTH) }) ) .mutation(async ({ input }) => { @@ -300,19 +304,19 @@ export const miscRouter = createTRPCRouter({ "content-type": "application/json" }, body: JSON.stringify(sendinblueData), - timeout: 15000 + timeout: NETWORK_CONFIG.EMAIL_API_TIMEOUT_MS }); await checkResponse(response); return response; }, { - maxRetries: 2, - retryDelay: 1000 + maxRetries: NETWORK_CONFIG.MAX_RETRIES, + retryDelay: NETWORK_CONFIG.RETRY_DELAY_MS } ); - const exp = new Date(Date.now() + 1 * 60 * 1000); + const exp = new Date(Date.now() + COOLDOWN_TIMERS.CONTACT_REQUEST_MS); setCookie("contactRequestSent", exp.toUTCString(), { expires: exp, path: "/" @@ -415,12 +419,15 @@ export const miscRouter = createTRPCRouter({ "content-type": "application/json" }, body: JSON.stringify(sendinblueMyData), - timeout: 15000 + timeout: NETWORK_CONFIG.EMAIL_API_TIMEOUT_MS }); await checkResponse(response); return response; }, - { maxRetries: 2, retryDelay: 1000 } + { + maxRetries: NETWORK_CONFIG.MAX_RETRIES, + retryDelay: NETWORK_CONFIG.RETRY_DELAY_MS + } ), fetchWithRetry( async () => { @@ -432,16 +439,19 @@ export const miscRouter = createTRPCRouter({ "content-type": "application/json" }, body: JSON.stringify(sendinblueUserData), - timeout: 15000 + timeout: NETWORK_CONFIG.EMAIL_API_TIMEOUT_MS }); await checkResponse(response); return response; }, - { maxRetries: 2, retryDelay: 1000 } + { + maxRetries: NETWORK_CONFIG.MAX_RETRIES, + retryDelay: NETWORK_CONFIG.RETRY_DELAY_MS + } ) ]); - const exp = new Date(Date.now() + 1 * 60 * 1000); + const exp = new Date(Date.now() + COOLDOWN_TIMERS.CONTACT_REQUEST_MS); setCookie("deletionRequestSent", exp.toUTCString(), { expires: exp, path: "/" diff --git a/src/server/api/schemas/user.ts b/src/server/api/schemas/user.ts index d8c0029..4108852 100644 --- a/src/server/api/schemas/user.ts +++ b/src/server/api/schemas/user.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { validatePassword } from "~/lib/validation"; +import { VALIDATION_CONFIG } from "~/config"; /** * User API Validation Schemas @@ -14,11 +15,14 @@ import { validatePassword } from "~/lib/validation"; /** * Secure password validation with strength requirements - * Minimum 12 characters, uppercase, lowercase, number, and special character + * Minimum length from config, uppercase, lowercase, number, and special character */ const securePasswordSchema = z .string() - .min(12, "Password must be at least 12 characters") + .min( + VALIDATION_CONFIG.MIN_PASSWORD_LENGTH, + `Password must be at least ${VALIDATION_CONFIG.MIN_PASSWORD_LENGTH} characters` + ) .refine( (password) => { const result = validatePassword(password); @@ -44,7 +48,7 @@ export const registerUserSchema = z .object({ email: z.string().email(), password: securePasswordSchema, - passwordConfirmation: z.string().min(12) + passwordConfirmation: z.string().min(VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) }) .refine((data) => data.password === data.passwordConfirmation, { message: "Passwords do not match", @@ -100,7 +104,9 @@ export const changePasswordSchema = z .object({ oldPassword: z.string().min(1, "Current password is required"), newPassword: securePasswordSchema, - newPasswordConfirmation: z.string().min(12) + newPasswordConfirmation: z + .string() + .min(VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) }) .refine((data) => data.newPassword === data.newPasswordConfirmation, { message: "Passwords do not match", @@ -117,7 +123,9 @@ export const changePasswordSchema = z export const setPasswordSchema = z .object({ newPassword: securePasswordSchema, - newPasswordConfirmation: z.string().min(12) + newPasswordConfirmation: z + .string() + .min(VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) }) .refine((data) => data.newPassword === data.newPasswordConfirmation, { message: "Passwords do not match", @@ -138,7 +146,9 @@ export const resetPasswordSchema = z .object({ token: z.string().min(1), newPassword: securePasswordSchema, - newPasswordConfirmation: z.string().min(12) + newPasswordConfirmation: z + .string() + .min(VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) }) .refine((data) => data.newPassword === data.newPasswordConfirmation, { message: "Passwords do not match", diff --git a/src/server/cache.ts b/src/server/cache.ts index 5c228a0..32e1351 100644 --- a/src/server/cache.ts +++ b/src/server/cache.ts @@ -1,3 +1,5 @@ +import { CACHE_CONFIG } from "~/config"; + interface CacheEntry { data: T; timestamp: number; @@ -80,7 +82,8 @@ export async function withCacheAndStale( logErrors?: boolean; } = {} ): Promise { - const { maxStaleMs = 7 * 24 * 60 * 60 * 1000, logErrors = true } = options; + const { maxStaleMs = CACHE_CONFIG.MAX_STALE_DATA_MS, logErrors = true } = + options; const cached = cache.get(key, ttlMs); if (cached !== null) { diff --git a/src/server/email.ts b/src/server/email.ts index e1f2156..7f9bd42 100644 --- a/src/server/email.ts +++ b/src/server/email.ts @@ -1,7 +1,8 @@ import { SignJWT } from "jose"; import { env } from "~/env/server"; +import { AUTH_CONFIG } from "~/config"; -export const LINEAGE_JWT_EXPIRY = "14d"; +export const LINEAGE_JWT_EXPIRY = AUTH_CONFIG.LINEAGE_JWT_EXPIRY; export async function sendEmailVerification(userEmail: string): Promise<{ success: boolean; diff --git a/src/server/security.ts b/src/server/security.ts index eeb4a39..fb556b1 100644 --- a/src/server/security.ts +++ b/src/server/security.ts @@ -4,6 +4,13 @@ import type { H3Event } from "vinxi/http"; import { t } from "~/server/api/utils"; import { logAuditEvent } from "~/server/audit"; import { env } from "~/env/server"; +import { + AUTH_CONFIG, + RATE_LIMITS as CONFIG_RATE_LIMITS, + RATE_LIMIT_CLEANUP_INTERVAL_MS, + ACCOUNT_LOCKOUT as CONFIG_ACCOUNT_LOCKOUT, + PASSWORD_RESET_CONFIG as CONFIG_PASSWORD_RESET +} from "~/config"; /** * Extract cookie value from H3Event (works in both production and tests) @@ -106,7 +113,7 @@ export function generateCSRFToken(): string { export function setCSRFToken(event: H3Event): string { const token = generateCSRFToken(); setCookieValue(event, "csrf-token", token, { - maxAge: 60 * 60 * 24 * 14, // 14 days - same as session + maxAge: AUTH_CONFIG.CSRF_TOKEN_MAX_AGE, path: "/", httpOnly: false, // Must be readable by client JS secure: env.NODE_ENV === "production", @@ -207,17 +214,14 @@ export function clearRateLimitStore(): void { /** * Cleanup expired rate limit entries every 5 minutes */ -setInterval( - () => { - const now = Date.now(); - for (const [key, record] of rateLimitStore.entries()) { - if (now > record.resetAt) { - rateLimitStore.delete(key); - } +setInterval(() => { + const now = Date.now(); + for (const [key, record] of rateLimitStore.entries()) { + if (now > record.resetAt) { + rateLimitStore.delete(key); } - }, - 5 * 60 * 1000 -); + } +}, RATE_LIMIT_CLEANUP_INTERVAL_MS); /** * Get client IP address from request headers @@ -320,19 +324,9 @@ export function checkRateLimit( /** * Rate limit configuration for different operations + * Re-exported from config for backward compatibility */ -export const RATE_LIMITS = { - // Login: 5 attempts per 15 minutes per IP - LOGIN_IP: { maxAttempts: 5, windowMs: 15 * 60 * 1000 }, - // Login: 3 attempts per hour per email - LOGIN_EMAIL: { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, - // Password reset: 3 attempts per hour per IP - PASSWORD_RESET_IP: { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, - // Registration: 3 attempts per hour per IP - REGISTRATION_IP: { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, - // Email verification: 5 attempts per 15 minutes per IP - EMAIL_VERIFICATION_IP: { maxAttempts: 5, windowMs: 15 * 60 * 1000 } -} as const; +export const RATE_LIMITS = CONFIG_RATE_LIMITS; /** * Rate limiting middleware for login operations @@ -405,11 +399,9 @@ export function rateLimitEmailVerification( /** * Account lockout configuration + * Re-exported from config for backward compatibility */ -export const ACCOUNT_LOCKOUT = { - MAX_FAILED_ATTEMPTS: 5, - LOCKOUT_DURATION_MS: 5 * 60 * 1000 // 5 minutes -} as const; +export const ACCOUNT_LOCKOUT = CONFIG_ACCOUNT_LOCKOUT; /** * Check if an account is locked @@ -527,10 +519,9 @@ export async function resetFailedAttempts(userId: string): Promise { /** * Password reset token configuration + * Re-exported from config for backward compatibility */ -export const PASSWORD_RESET_CONFIG = { - TOKEN_EXPIRY_MS: 60 * 60 * 1000 // 1 hour -} as const; +export const PASSWORD_RESET_CONFIG = CONFIG_PASSWORD_RESET; /** * Create a password reset token