config centralized

This commit is contained in:
Michael Freno
2026-01-01 02:22:33 -05:00
parent a6417c650f
commit 8e77727148
24 changed files with 519 additions and 143 deletions

View File

@@ -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<string> {
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: "/"
}
);

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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)");

View File

@@ -20,6 +20,7 @@ import {
TimeoutError,
APIError
} from "~/server/fetch-utils";
import { NETWORK_CONFIG, COOLDOWN_TIMERS, VALIDATION_CONFIG } from "~/config";
const assets: Record<string, string> = {
"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: "/"

View File

@@ -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",