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