idk h3 at all

This commit is contained in:
Michael Freno
2025-12-28 21:56:34 -05:00
parent 4892dff38e
commit f5abf77454
2 changed files with 190 additions and 148 deletions

View File

@@ -30,6 +30,7 @@ import {
setCSRFToken,
csrfProtection,
getClientIP,
getUserAgent,
getAuditContext,
rateLimitLogin,
rateLimitPasswordReset,
@@ -37,6 +38,22 @@ import {
rateLimitEmailVerification
} from "~/server/security";
import { logAuditEvent } from "~/server/audit";
import type { H3Event } from "vinxi/http";
import type { Context } from "../utils";
/**
* Safely extract H3Event from Context
* In production: ctx.event is APIEvent, H3Event is at ctx.event.nativeEvent
* In development: ctx.event might be H3Event directly
*/
function getH3Event(ctx: Context): H3Event {
// Check if nativeEvent exists (production)
if (ctx.event && 'nativeEvent' in ctx.event && ctx.event.nativeEvent) {
return ctx.event.nativeEvent as H3Event;
}
// Otherwise, assume ctx.event is H3Event (development)
return ctx.event as unknown as H3Event;
}
/**
* Create JWT with session tracking
@@ -115,7 +132,7 @@ function setAuthCookies(
const cookieOptions: any = {
path: "/",
httpOnly: true,
secure: true, // Always enforce secure cookies
secure: env.NODE_ENV === "production",
sameSite: "lax",
...options
};
@@ -292,9 +309,9 @@ export const authRouter = createTRPCRouter({
}
// Create session with client info
const clientIP = getClientIP(ctx.event.nativeEvent);
const clientIP = getClientIP(getH3Event(ctx));
const userAgent =
ctx.event.nativeEvent.request.headers.get("user-agent") || "unknown";
getUserAgent(getH3Event(ctx));
const sessionId = await createSession(
userId,
"14d",
@@ -304,16 +321,16 @@ export const authRouter = createTRPCRouter({
const token = await createJWT(userId, sessionId);
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
setCookie(getH3Event(ctx), "userIDToken", token, {
maxAge: 60 * 60 * 24 * 14, // 14 days
path: "/",
httpOnly: true,
secure: true, // Always enforce secure cookies
secure: env.NODE_ENV === "production",
sameSite: "lax"
});
// Set CSRF token for authenticated session
setCSRFToken(ctx.event.nativeEvent);
setCSRFToken(getH3Event(ctx));
// Log successful OAuth login
await logAuditEvent({
@@ -331,7 +348,7 @@ export const authRouter = createTRPCRouter({
};
} catch (error) {
// Log failed OAuth login
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.login.failed",
eventData: {
@@ -475,9 +492,9 @@ export const authRouter = createTRPCRouter({
}
// Create session with client info
const clientIP = getClientIP(ctx.event.nativeEvent);
const clientIP = getClientIP(getH3Event(ctx));
const userAgent =
ctx.event.nativeEvent.request.headers.get("user-agent") || "unknown";
getUserAgent(getH3Event(ctx));
const sessionId = await createSession(
userId,
"14d",
@@ -487,16 +504,16 @@ export const authRouter = createTRPCRouter({
const token = await createJWT(userId, sessionId);
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
setCookie(getH3Event(ctx), "userIDToken", token, {
maxAge: 60 * 60 * 24 * 14, // 14 days
path: "/",
httpOnly: true,
secure: true, // Always enforce secure cookies
secure: env.NODE_ENV === "production",
sameSite: "lax"
});
// Set CSRF token for authenticated session
setCSRFToken(ctx.event.nativeEvent);
setCSRFToken(getH3Event(ctx));
// Log successful OAuth login
await logAuditEvent({
@@ -514,7 +531,7 @@ export const authRouter = createTRPCRouter({
};
} catch (error) {
// Log failed OAuth login
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.login.failed",
eventData: {
@@ -595,9 +612,9 @@ export const authRouter = createTRPCRouter({
const userId = (res.rows[0] as unknown as User).id;
// Create session with client info
const clientIP = getClientIP(ctx.event.nativeEvent);
const clientIP = getClientIP(getH3Event(ctx));
const userAgent =
ctx.event.nativeEvent.request.headers.get("user-agent") || "unknown";
getUserAgent(getH3Event(ctx));
const expiresIn = rememberMe ? "14d" : "12h";
const sessionId = await createSession(
userId,
@@ -611,7 +628,7 @@ export const authRouter = createTRPCRouter({
const cookieOptions: any = {
path: "/",
httpOnly: true,
secure: true, // Always enforce secure cookies
secure: env.NODE_ENV === "production",
sameSite: "lax"
};
@@ -620,14 +637,14 @@ export const authRouter = createTRPCRouter({
}
setCookie(
ctx.event.nativeEvent,
getH3Event(ctx),
"userIDToken",
userToken,
cookieOptions
);
// Set CSRF token for authenticated session
setCSRFToken(ctx.event.nativeEvent);
setCSRFToken(getH3Event(ctx));
// Log successful email link login
await logAuditEvent({
@@ -645,7 +662,7 @@ export const authRouter = createTRPCRouter({
};
} catch (error) {
// Log failed email link login
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.login.failed",
eventData: {
@@ -704,10 +721,10 @@ export const authRouter = createTRPCRouter({
await conn.execute({ sql: query, args: params });
// Log successful email verification
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId,
eventType: "auth.email_verified",
eventType: "auth.email.verify.complete",
eventData: { email },
ipAddress,
userAgent,
@@ -720,9 +737,9 @@ export const authRouter = createTRPCRouter({
};
} catch (error) {
// Log failed email verification
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.email_verified",
eventType: "auth.email.verify.complete",
eventData: {
email,
reason: error instanceof TRPCError ? error.message : "unknown"
@@ -749,8 +766,8 @@ export const authRouter = createTRPCRouter({
const { email, password, passwordConfirmation } = input;
// Apply rate limiting
const clientIP = getClientIP(ctx.event.nativeEvent);
rateLimitRegistration(clientIP, ctx.event.nativeEvent);
const clientIP = getClientIP(getH3Event(ctx));
rateLimitRegistration(clientIP, getH3Event(ctx));
// Schema already validates password match, but double check
if (password !== passwordConfirmation) {
@@ -771,9 +788,9 @@ export const authRouter = createTRPCRouter({
});
// Create session with client info
const clientIP = getClientIP(ctx.event.nativeEvent);
const clientIP = getClientIP(getH3Event(ctx));
const userAgent =
ctx.event.nativeEvent.request.headers.get("user-agent") || "unknown";
getUserAgent(getH3Event(ctx));
const sessionId = await createSession(
userId,
"14d",
@@ -783,21 +800,21 @@ export const authRouter = createTRPCRouter({
const token = await createJWT(userId, sessionId);
setCookie(ctx.event.nativeEvent, "userIDToken", token, {
setCookie(getH3Event(ctx), "userIDToken", token, {
maxAge: 60 * 60 * 24 * 14, // 14 days
path: "/",
httpOnly: true,
secure: true, // Always enforce secure cookies
secure: env.NODE_ENV === "production",
sameSite: "lax"
});
// Set CSRF token for authenticated session
setCSRFToken(ctx.event.nativeEvent);
setCSRFToken(getH3Event(ctx));
// Log successful registration
await logAuditEvent({
userId,
eventType: "auth.registration.success",
eventType: "auth.register.success",
eventData: { email, method: "email" },
ipAddress: clientIP,
userAgent,
@@ -807,9 +824,9 @@ export const authRouter = createTRPCRouter({
return { success: true, message: "success" };
} catch (e) {
// Log failed registration
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.registration.failed",
eventType: "auth.register.failed",
eventData: {
email,
method: "email",
@@ -831,107 +848,131 @@ export const authRouter = createTRPCRouter({
emailPasswordLogin: publicProcedure
.input(loginUserSchema)
.mutation(async ({ input, ctx }) => {
const { email, password, rememberMe } = input;
try {
const { email, password, rememberMe } = input;
// Apply rate limiting
const clientIP = getClientIP(ctx.event.nativeEvent);
rateLimitLogin(email, clientIP, ctx.event.nativeEvent);
// Apply rate limiting
const clientIP = getClientIP(getH3Event(ctx));
rateLimitLogin(email, clientIP, getH3Event(ctx));
const conn = ConnectionFactory();
const res = await conn.execute({
sql: "SELECT * FROM User WHERE email = ?",
args: [email]
});
const conn = ConnectionFactory();
const res = await conn.execute({
sql: "SELECT * FROM User WHERE email = ?",
args: [email]
});
// Always run password check to prevent timing attacks
const user =
res.rows.length > 0 ? (res.rows[0] as unknown as User) : null;
const passwordHash = user?.password_hash || null;
const passwordMatch = await checkPasswordSafe(password, passwordHash);
// Always run password check to prevent timing attacks
const user =
res.rows.length > 0 ? (res.rows[0] as unknown as User) : null;
const passwordHash = user?.password_hash || null;
const passwordMatch = await checkPasswordSafe(password, passwordHash);
// Check all conditions after password verification
if (!user || !passwordHash || !passwordMatch) {
// Log failed login attempt (wrap in try-catch to ensure it never blocks auth flow)
// Check all conditions after password verification
if (!user || !passwordHash || !passwordMatch) {
// Debug logging (remove after fixing)
console.log("Login failed for:", email);
console.log("User found:", !!user);
console.log("Password hash exists:", !!passwordHash);
console.log("Password match:", passwordMatch);
// Log failed login attempt (wrap in try-catch to ensure it never blocks auth flow)
try {
const { ipAddress, userAgent } = getAuditContext(
getH3Event(ctx)
);
await logAuditEvent({
eventType: "auth.login.failed",
eventData: {
email,
method: "password",
reason: "invalid_credentials"
},
ipAddress,
userAgent,
success: false
});
} catch (auditError) {
console.error("Audit logging failed:", auditError);
}
throw new TRPCError({
code: "UNAUTHORIZED",
message: "no-match"
});
}
if (
!user.provider ||
!["email", "google", "github", "apple"].includes(user.provider)
) {
await conn.execute({
sql: "UPDATE User SET provider = ? WHERE id = ?",
args: ["email", user.id]
});
}
const expiresIn = rememberMe ? "14d" : "12h";
// Create session with client info (reuse clientIP from rate limiting)
const userAgent =
getUserAgent(getH3Event(ctx));
const sessionId = await createSession(
user.id,
expiresIn,
clientIP,
userAgent
);
const token = await createJWT(user.id, sessionId, expiresIn);
const cookieOptions: any = {
path: "/",
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "lax"
};
if (rememberMe) {
cookieOptions.maxAge = 60 * 60 * 24 * 14; // 14 days
}
setCookie(getH3Event(ctx), "userIDToken", token, cookieOptions);
// Set CSRF token for authenticated session
setCSRFToken(getH3Event(ctx));
// Log successful login (wrap in try-catch to ensure it never blocks auth flow)
try {
const { ipAddress, userAgent } = getAuditContext(
ctx.event.nativeEvent
);
await logAuditEvent({
eventType: "auth.login.failed",
eventData: {
email,
method: "password",
reason: "invalid_credentials"
},
ipAddress,
userId: user.id,
eventType: "auth.login.success",
eventData: { method: "password", rememberMe: rememberMe || false },
ipAddress: clientIP,
userAgent,
success: false
success: true
});
} catch (auditError) {
console.error("Audit logging failed:", auditError);
}
return { success: true, message: "success" };
} catch (error) {
// Log the actual error for debugging
console.error("emailPasswordLogin error:", error);
console.error("Error stack:", error instanceof Error ? error.stack : "no stack");
// Re-throw TRPCErrors as-is
if (error instanceof TRPCError) {
throw error;
}
// Wrap other errors
throw new TRPCError({
code: "UNAUTHORIZED",
message: "no-match"
code: "INTERNAL_SERVER_ERROR",
message: "An error occurred during login",
cause: error
});
}
if (
!user.provider ||
!["email", "google", "github", "apple"].includes(user.provider)
) {
await conn.execute({
sql: "UPDATE User SET provider = ? WHERE id = ?",
args: ["email", user.id]
});
}
const expiresIn = rememberMe ? "14d" : "12h";
// Create session with client info (reuse clientIP from rate limiting)
const userAgent =
ctx.event.nativeEvent.request.headers.get("user-agent") || "unknown";
const sessionId = await createSession(
user.id,
expiresIn,
clientIP,
userAgent
);
const token = await createJWT(user.id, sessionId, expiresIn);
const cookieOptions: any = {
path: "/",
httpOnly: true,
secure: true, // Always enforce secure cookies
sameSite: "lax"
};
if (rememberMe) {
cookieOptions.maxAge = 60 * 60 * 24 * 14; // 14 days
}
setCookie(ctx.event.nativeEvent, "userIDToken", token, cookieOptions);
// Set CSRF token for authenticated session
setCSRFToken(ctx.event.nativeEvent);
// Log successful login (wrap in try-catch to ensure it never blocks auth flow)
try {
await logAuditEvent({
userId: user.id,
eventType: "auth.login.success",
eventData: { method: "password", rememberMe: rememberMe || false },
ipAddress: clientIP,
userAgent,
success: true
});
} catch (auditError) {
console.error("Audit logging failed:", auditError);
}
return { success: true, message: "success" };
}),
requestEmailLinkLogin: publicProcedure
@@ -946,7 +987,7 @@ export const authRouter = createTRPCRouter({
try {
const requested = getCookie(
ctx.event.nativeEvent,
getH3Event(ctx),
"emailLoginLinkRequested"
);
if (requested) {
@@ -1025,7 +1066,7 @@ export const authRouter = createTRPCRouter({
const exp = new Date(Date.now() + 2 * 60 * 1000);
setCookie(
ctx.event.nativeEvent,
getH3Event(ctx),
"emailLoginLinkRequested",
exp.toUTCString(),
{
@@ -1066,12 +1107,12 @@ export const authRouter = createTRPCRouter({
const { email } = input;
// Apply rate limiting
const clientIP = getClientIP(ctx.event.nativeEvent);
rateLimitPasswordReset(clientIP, ctx.event.nativeEvent);
const clientIP = getClientIP(getH3Event(ctx));
rateLimitPasswordReset(clientIP, getH3Event(ctx));
try {
const requested = getCookie(
ctx.event.nativeEvent,
getH3Event(ctx),
"passwordResetRequested"
);
if (requested) {
@@ -1145,7 +1186,7 @@ export const authRouter = createTRPCRouter({
const exp = new Date(Date.now() + 5 * 60 * 1000);
setCookie(
ctx.event.nativeEvent,
getH3Event(ctx),
"passwordResetRequested",
exp.toUTCString(),
{
@@ -1155,10 +1196,10 @@ export const authRouter = createTRPCRouter({
);
// Log password reset request
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId: user.id,
eventType: "auth.password_reset.requested",
eventType: "auth.password.reset.request",
eventData: { email },
ipAddress,
userAgent,
@@ -1172,10 +1213,10 @@ export const authRouter = createTRPCRouter({
!(error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS")
) {
const { ipAddress, userAgent } = getAuditContext(
ctx.event.nativeEvent
getH3Event(ctx)
);
await logAuditEvent({
eventType: "auth.password_reset.requested",
eventType: "auth.password.reset.request",
eventData: {
email: input.email,
reason: error instanceof TRPCError ? error.message : "unknown"
@@ -1266,20 +1307,20 @@ export const authRouter = createTRPCRouter({
});
}
setCookie(ctx.event.nativeEvent, "emailToken", "", {
setCookie(getH3Event(ctx), "emailToken", "", {
maxAge: 0,
path: "/"
});
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
setCookie(getH3Event(ctx), "userIDToken", "", {
maxAge: 0,
path: "/"
});
// Log successful password reset
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId: payload.id,
eventType: "auth.password_reset.completed",
eventType: "auth.password.reset.complete",
eventData: {},
ipAddress,
userAgent,
@@ -1289,9 +1330,9 @@ export const authRouter = createTRPCRouter({
return { success: true, message: "success" };
} catch (error) {
// Log failed password reset
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.password_reset.completed",
eventType: "auth.password.reset.complete",
eventData: {
reason: error instanceof TRPCError ? error.message : "unknown"
},
@@ -1317,12 +1358,12 @@ export const authRouter = createTRPCRouter({
const { email } = input;
// Apply rate limiting
const clientIP = getClientIP(ctx.event.nativeEvent);
rateLimitEmailVerification(clientIP, ctx.event.nativeEvent);
const clientIP = getClientIP(getH3Event(ctx));
rateLimitEmailVerification(clientIP, getH3Event(ctx));
try {
const requested = getCookie(
ctx.event.nativeEvent,
getH3Event(ctx),
"emailVerificationRequested"
);
if (requested) {
@@ -1399,7 +1440,7 @@ export const authRouter = createTRPCRouter({
await sendEmail(email, "freno.me email verification", htmlContent);
setCookie(
ctx.event.nativeEvent,
getH3Event(ctx),
"emailVerificationRequested",
Date.now().toString(),
{
@@ -1409,10 +1450,10 @@ export const authRouter = createTRPCRouter({
);
// Log email verification request
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId: user.id,
eventType: "auth.email_verification.requested",
eventType: "auth.email.verify.request",
eventData: { email },
ipAddress,
userAgent,
@@ -1426,10 +1467,10 @@ export const authRouter = createTRPCRouter({
!(error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS")
) {
const { ipAddress, userAgent } = getAuditContext(
ctx.event.nativeEvent
getH3Event(ctx)
);
await logAuditEvent({
eventType: "auth.email_verification.requested",
eventType: "auth.email.verify.request",
eventData: {
email: input.email,
reason: error instanceof TRPCError ? error.message : "unknown"
@@ -1468,7 +1509,7 @@ export const authRouter = createTRPCRouter({
// Try to get user ID for audit log before clearing cookies
let userId: string | null = null;
try {
const token = getCookie(ctx.event.nativeEvent, "userIDToken");
const token = getCookie(getH3Event(ctx), "userIDToken");
if (token) {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const { payload } = await jwtVerify(token, secret);
@@ -1478,17 +1519,17 @@ export const authRouter = createTRPCRouter({
// Ignore token verification errors during signout
}
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
setCookie(getH3Event(ctx), "userIDToken", "", {
maxAge: 0,
path: "/"
});
setCookie(ctx.event.nativeEvent, "emailToken", "", {
setCookie(getH3Event(ctx), "emailToken", "", {
maxAge: 0,
path: "/"
});
// Log signout
const { ipAddress, userAgent } = getAuditContext(ctx.event.nativeEvent);
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId,
eventType: "auth.logout",

View File

@@ -3,6 +3,7 @@ import { getCookie, setCookie } from "vinxi/http";
import type { H3Event } from "vinxi/http";
import { t } from "~/server/api/utils";
import { logAuditEvent } from "~/server/audit";
import { env } from "~/env/server";
/**
* Extract cookie value from H3Event (works in both production and tests)
@@ -108,7 +109,7 @@ export function setCSRFToken(event: H3Event): string {
maxAge: 60 * 60 * 24 * 14, // 14 days - same as session
path: "/",
httpOnly: false, // Must be readable by client JS
secure: true, // Always enforce secure
secure: env.NODE_ENV === "production",
sameSite: "lax"
});
return token;