chore: remediate pygienium audit findings

Dead code: 77 verified-unused exports, files (BackArrow, MenuBars,
cookies.ts, db/create.ts, schemas/comment.ts, security-headers.ts) and
12 unused dependencies removed
Comments: ~370 RESTATE comments stripped across 53 files; 2 verbose
blocks tightened; dead commented-out config removed
Complexity: bulkUpsert extracted into 11 per-entity helpers (CCN 156->~10);
login formHandler split into 3 submitters (CCN 63->~5); account page render
split into 8 section components (CCN 40); updatePost SQL builder rebuilt;
assert*Owned consolidated behind generic assertOwnedBy
Defensive guards: 4 redundant rethrow/nullish guards removed
This commit is contained in:
2026-08-11 13:36:18 -04:00
parent 33ca9213f2
commit 898c891bd5
76 changed files with 1437 additions and 2600 deletions

View File

@@ -117,14 +117,11 @@ function scheduleAnalyticsFlush(): void {
*/
export async function logVisit(entry: AnalyticsEntry): Promise<void> {
try {
// Add to buffer
analyticsBuffer.entries.push(entry);
// Flush if batch size reached
if (analyticsBuffer.entries.length >= CACHE_CONFIG.ANALYTICS_BATCH_SIZE) {
await flushAnalyticsBuffer();
} else {
// Schedule periodic flush
scheduleAnalyticsFlush();
}
} catch (error) {
@@ -422,7 +419,6 @@ export async function getPerformanceStats(days: number = 30): Promise<{
}> {
const conn = ConnectionFactory();
// Get average metrics
const avgResult = await conn.execute({
sql: `SELECT
AVG(lcp) as avgLcp,
@@ -472,7 +468,6 @@ export async function getPerformanceStats(days: number = 30): Promise<{
args: []
});
// Get performance by path (only for non-API paths)
const byPathResult = await conn.execute({
sql: `SELECT
path,

View File

@@ -80,11 +80,9 @@ import {
* 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;
}
@@ -245,7 +243,6 @@ export const authRouter = createTRPCRouter({
try {
await conn.execute({ sql: insertQuery, args: insertParams });
// Also create UserProvider entry for new user
await linkProvider(userId, "github", {
providerUserId: login,
email: email,
@@ -300,7 +297,6 @@ export const authRouter = createTRPCRouter({
} catch (error) {
console.error("[GitHub Callback] Error during OAuth flow:", error);
// Log failed OAuth login
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.login.failed",
@@ -452,7 +448,6 @@ export const authRouter = createTRPCRouter({
args: insertParams
});
// Also create UserProvider entry for new user
await linkProvider(userId, "google", {
providerUserId: email,
email: email,
@@ -481,7 +476,6 @@ export const authRouter = createTRPCRouter({
}
}
// Issue JWT (OAuth defaults to remember me)
const event = getH3Event(ctx);
const clientIP = getClientIP(event);
const userAgent = getUserAgent(event);
@@ -631,7 +625,6 @@ export const authRouter = createTRPCRouter({
} catch (error) {
console.error("[Email Login] Error during login:", error);
// Log failed email link login
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.login.failed",
@@ -685,10 +678,7 @@ export const authRouter = createTRPCRouter({
});
}
// Check if there's a valid JWT token with this code
// We need to find the token that was generated for this email
// Since we can't store tokens in DB efficiently, we'll verify against the cookie
// Get the token from cookie (we'll store it when sending email)
// Tokens aren't stored in DB; verify the code against the JWT cookie set when the email was sent
const storedToken = getCookie(getH3Event(ctx), "emailLoginToken");
if (!storedToken) {
throw new TRPCError({
@@ -697,7 +687,6 @@ export const authRouter = createTRPCRouter({
});
}
// Verify the JWT and check the code
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
let payload;
try {
@@ -756,7 +745,6 @@ export const authRouter = createTRPCRouter({
} catch (error) {
console.error("[Email Code Login] Error during login:", error);
// Log failed code login
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.login.failed",
@@ -808,7 +796,6 @@ export const authRouter = createTRPCRouter({
const conn = ConnectionFactory();
// Get user ID for audit log
const userRes = await conn.execute({
sql: "SELECT id FROM User WHERE email = ?",
args: [email]
@@ -819,7 +806,6 @@ export const authRouter = createTRPCRouter({
const params = [true, email];
await conn.execute({ sql: query, args: params });
// Log successful email verification
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId,
@@ -835,7 +821,6 @@ export const authRouter = createTRPCRouter({
message: "Email verification success, you may close this window"
};
} catch (error) {
// Log failed email verification
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.email.verify.complete",
@@ -864,7 +849,6 @@ export const authRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { email, password, passwordConfirmation, rememberMe } = input;
// Apply rate limiting
const clientIP = getClientIP(getH3Event(ctx));
await rateLimitRegistration(clientIP, getH3Event(ctx));
@@ -876,10 +860,8 @@ export const authRouter = createTRPCRouter({
});
}
// Check if email already exists (User table or UserProvider table)
const existingUserId = await findUserByEmail(email);
if (existingUserId) {
// User exists - check if they have a password
const conn = ConnectionFactory();
const userCheck = await conn.execute({
sql: "SELECT password_hash, provider FROM User WHERE id = ?",
@@ -916,13 +898,11 @@ export const authRouter = createTRPCRouter({
args: [userId, email, passwordHash, "email"]
});
// Create UserProvider entry for email auth
await linkProvider(userId, "email", {
providerUserId: email,
email: email
});
// Issue auth token with client info
const event = getH3Event(ctx);
const clientIP = getClientIP(event);
const userAgent = getUserAgent(event);
@@ -930,13 +910,11 @@ export const authRouter = createTRPCRouter({
await issueAuthToken({
event,
userId,
rememberMe: rememberMe ?? true
rememberMe,
});
// Set CSRF token
setCSRFToken(event);
// Log successful registration
await logAuditEvent({
userId,
eventType: "auth.register.success",
@@ -948,7 +926,6 @@ export const authRouter = createTRPCRouter({
return { success: true, message: "success" };
} catch (e) {
// Log failed registration
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.register.failed",
@@ -976,7 +953,6 @@ export const authRouter = createTRPCRouter({
try {
const { email, password, rememberMe } = input;
// Apply rate limiting
const clientIP = getClientIP(getH3Event(ctx));
await rateLimitLogin(email, clientIP, getH3Event(ctx));
@@ -992,9 +968,7 @@ export const authRouter = createTRPCRouter({
const passwordHash = user?.password_hash || null;
const passwordMatch = await checkPasswordSafe(password, passwordHash);
// Check all conditions after password verification
if (!user || !passwordHash || !passwordMatch) {
// Record failed login attempt if user exists
if (user?.id) {
const lockoutStatus = await recordFailedLogin(user.id);
@@ -1032,7 +1006,6 @@ export const authRouter = createTRPCRouter({
}
}
// Log failed login attempt
try {
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
@@ -1060,7 +1033,6 @@ export const authRouter = createTRPCRouter({
});
}
// Check if account is locked before allowing login
const lockoutCheck = await checkAccountLockout(user.id);
if (lockoutCheck.isLocked) {
const remainingSec = Math.ceil(
@@ -1083,22 +1055,18 @@ export const authRouter = createTRPCRouter({
});
}
// Reset failed attempts on successful login
await resetFailedAttempts(user.id);
// Reset rate limits on successful login
await resetLoginRateLimits(email, clientIP);
// Issue JWT for authenticated user
const event = getH3Event(ctx);
const userAgent = getUserAgent(event);
await issueAuthToken({
event,
userId: user.id,
rememberMe: rememberMe ?? false
rememberMe,
});
// Set CSRF token for authenticated user
setCSRFToken(event);
// Log successful login (wrap in try-catch to ensure it never blocks auth flow)
@@ -1106,7 +1074,7 @@ export const authRouter = createTRPCRouter({
await logAuditEvent({
userId: user.id,
eventType: "auth.login.success",
eventData: { method: "password", rememberMe: rememberMe ?? false },
eventData: { method: "password", rememberMe },
ipAddress: clientIP,
userAgent,
success: true
@@ -1251,7 +1219,6 @@ export const authRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { email } = input;
// Apply rate limiting
const clientIP = getClientIP(getH3Event(ctx));
await rateLimitPasswordReset(clientIP, getH3Event(ctx));
@@ -1303,7 +1270,6 @@ export const authRouter = createTRPCRouter({
}
);
// Log password reset request
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId: user.id,
@@ -1316,7 +1282,6 @@ export const authRouter = createTRPCRouter({
return { success: true, message: "email sent" };
} catch (error) {
// Log failed password reset request (only if not rate limited)
if (
!(error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS")
) {
@@ -1371,7 +1336,6 @@ export const authRouter = createTRPCRouter({
}
try {
// Validate and consume the password reset token
const tokenValidation = await validatePasswordResetToken(token);
if (!tokenValidation) {
@@ -1415,10 +1379,8 @@ export const authRouter = createTRPCRouter({
});
}
// Mark token as used
await markPasswordResetTokenUsed(tokenId);
// Log successful password reset
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId: userId,
@@ -1431,7 +1393,6 @@ export const authRouter = createTRPCRouter({
return { success: true, message: "success" };
} catch (error) {
// Log failed password reset
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
eventType: "auth.password.reset.complete",
@@ -1459,7 +1420,6 @@ export const authRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const { email } = input;
// Apply rate limiting
const clientIP = getClientIP(getH3Event(ctx));
await rateLimitEmailVerification(clientIP, getH3Event(ctx));
@@ -1520,7 +1480,6 @@ export const authRouter = createTRPCRouter({
}
);
// Log email verification request
const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx));
await logAuditEvent({
userId: user.id,
@@ -1533,7 +1492,6 @@ export const authRouter = createTRPCRouter({
return { success: true, message: "Verification email sent" };
} catch (error) {
// Log failed email verification request (only if not rate limited)
if (
!(error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS")
) {

View File

@@ -462,58 +462,50 @@ export const databaseRouter = createTRPCRouter({
}
}
let query = "UPDATE Post SET ";
let sets: string[] = [];
let params: any[] = [];
let first = true;
if (input.title !== undefined && input.title !== null) {
query += first ? "title = ?" : ", title = ?";
sets.push("title = ?");
params.push(input.title);
first = false;
}
if (input.subtitle !== undefined && input.subtitle !== null) {
query += first ? "subtitle = ?" : ", subtitle = ?";
sets.push("subtitle = ?");
params.push(input.subtitle);
first = false;
}
if (input.body !== undefined && input.body !== null) {
query += first ? "body = ?" : ", body = ?";
sets.push("body = ?");
params.push(input.body);
first = false;
}
if (input.banner_photo !== undefined && input.banner_photo !== null) {
query += first ? "banner_photo = ?" : ", banner_photo = ?";
sets.push("banner_photo = ?");
if (input.banner_photo === "_DELETE_IMAGE_") {
params.push(null);
} else {
params.push(env.VITE_AWS_BUCKET_STRING + input.banner_photo);
}
first = false;
}
if (input.published !== undefined && input.published !== null) {
query += first ? "published = ?" : ", published = ?";
sets.push("published = ?");
params.push(input.published);
first = false;
}
if (shouldSetPublishDate) {
query += first ? "date = ?" : ", date = ?";
sets.push("date = ?");
params.push(new Date().toISOString());
first = false;
}
query += first ? "last_edited_date = ?" : ", last_edited_date = ?";
sets.push("last_edited_date = ?");
params.push(new Date().toISOString());
first = false;
query += first ? "author_id = ?" : ", author_id = ?";
sets.push("author_id = ?");
params.push(input.author_id);
query += " WHERE id = ?";
let query = "UPDATE Post SET " + sets.join(", ") + " WHERE id = ?";
params.push(input.id);
const results = await conn.execute({ sql: query, args: params });

View File

@@ -36,7 +36,6 @@ async function getLatestDMG(
throw new Error(`No DMG files found in S3 with prefix ${prefix}`);
}
// Filter for .dmg files only and sort by LastModified (newest first)
const dmgFiles = response.Contents.filter((obj) =>
obj.Key?.endsWith(".dmg")
).sort((a, b) => {
@@ -103,7 +102,6 @@ export const downloadsRouter = createTRPCRouter({
} else if (input.asset_name === "inputhalo") {
fileKey = await getLatestInputHaloDMG(client, bucket);
} else {
// Use static mapping for other assets
fileKey = assets[input.asset_name];
if (!fileKey) {

View File

@@ -33,7 +33,6 @@ export const gitActivityRouter = createTRPCRouter({
`github-commits-${input.limit}`,
CACHE_CONFIG.GIT_ACTIVITY_CACHE_TTL_MS,
async () => {
// Use Events API to get recent push events
const eventsResponse = await fetchWithTimeout(
`https://api.github.com/users/MikeFreno/events/public?per_page=100`,
{
@@ -48,7 +47,6 @@ export const gitActivityRouter = createTRPCRouter({
await checkResponse(eventsResponse);
const events = await eventsResponse.json();
// Collect (repo, sha) pairs from push events up front
const toFetch: { repoName: string; sha: string }[] = [];
for (const event of events) {
if (event.type !== "PushEvent") continue;

View File

@@ -72,13 +72,7 @@ export function assertS3KeyOwnership(key: string, userId: string | null): void {
// Account-deletion request email — product-aware
// ============================================================
//
// Pure helpers live in `./deletion-email.ts` (env-free) so they can be unit-
// tested in `bun:test` without a populated `.env`. Re-exported here for the
// tRPC mutation below + for callers that already import from `misc`.
// Import into local scope FIRST — `sendDeletionRequestEmail` below uses
// these names directly. A bare `export { ... } from` re-export does NOT make
// the bindings available locally, which caused a ReferenceError that crashed
// the entire tRPC router (503 on every /api/trpc call).
// Bare "export … from" doesn't bind names locally — import first or the router throws ReferenceError (503s every /api/trpc call)
import {
DELETION_PRODUCT_SCHEMA,
deletionCookieName,
@@ -256,7 +250,6 @@ export const miscRouter = createTRPCRouter({
lastModified: item.LastModified?.toISOString() || ""
})) || [];
// Filter out thumbnail files (ending with -small.ext)
const mainFiles = files.filter(
(file) => !file.key.match(/-small\.(jpg|jpeg|png|gif)$/i)
);
@@ -376,7 +369,6 @@ export const miscRouter = createTRPCRouter({
})
)
.mutation(async ({ input }) => {
// Verify Cloudflare Turnstile token
const turnstileValid = await verifyTurnstileToken(
input.turnstileToken,
env.TURNSTILE_SECRET_KEY,

View File

@@ -10,6 +10,7 @@
*/
import { describe, it, expect, mock, beforeEach } from "bun:test";
import { TRPCError } from "@trpc/server";
import type { Client } from "@libsql/client/web";
// Prevent the env/server.ts client-side guard from throwing during tests
@@ -288,7 +289,6 @@ describe("static audit: every targeted mutation handler uses ctx", () => {
const source = await Bun.file(import.meta.dir + "/nessa.ts").text();
for (const name of MUTATIONS) {
// Match: name: nessaProcedure ... .mutation(async ({ input }) — but NOT ({ input, ctx
const re = new RegExp(
`${name}:\\s*nessaProcedure[^}]*\\.mutation\\(async \\({\\s*input\\s*}\\)`,
"s"
@@ -305,7 +305,6 @@ describe("static audit: every targeted mutation handler uses ctx", () => {
const source = await Bun.file(import.meta.dir + "/nessa.ts").text();
for (const name of MUTATIONS) {
// Find the block for this mutation and check it references ctx
const re = new RegExp(
`${name}:\\s*nessaProcedure[\\s\\S]*?\\.mutation\\([\\s\\S]*?\\n \\}\\),`,
"s"
@@ -318,12 +317,55 @@ describe("static audit: every targeted mutation handler uses ctx", () => {
}
});
it("bulkUpsert filters exerciseLibrary by userId", async () => {
const source = await Bun.file(import.meta.dir + "/nessa.ts").text();
const bulkSection = source.match(
/if \(input\.exerciseLibrary\?\.length\) \{[\s\S]*?\n {8}\}/
);
expect(bulkSection).toBeTruthy();
expect(bulkSection![0]).toContain("userId !== ctx.nessaUserId");
it("upsertExerciseLibrary rejects an exercise owned by another user", async () => {
const mod = await import("./nessa");
const conn = makeMockConn([]);
const exercise = {
id: EXERCISE_ID,
userId: USER_B,
name: "Squat",
category: "Strength"
};
let caught: unknown;
try {
await mod.upsertExerciseLibrary(conn, USER_A, [exercise]);
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(TRPCError);
expect((caught as TRPCError).code).toBe("FORBIDDEN");
expect((caught as TRPCError).message).toBe("User mismatch");
});
it("upsertExerciseLibrary upserts an exercise owned by the caller", async () => {
const mod = await import("./nessa");
const conn = makeMockConn([{ userId: USER_A }]);
const exercise = {
id: EXERCISE_ID,
userId: USER_A,
name: "Squat",
category: "Strength"
};
await expect(
mod.upsertExerciseLibrary(conn, USER_A, [exercise])
).resolves.toBeUndefined();
expect(conn.execute).toHaveBeenCalledWith({
sql: expect.stringContaining(
"INSERT INTO exerciseLibrary (id, userId, name, category"
),
args: [
EXERCISE_ID,
USER_A,
"Squat",
"Strength",
null,
null,
null,
null,
null,
null,
null
]
});
});
});

View File

@@ -7,25 +7,45 @@ import type { Client } from "@libsql/client/web";
const NESSA_CACHE_TTL_MS = 5 * 60 * 1000;
/**
* Assert that the record identified by id in the given table belongs to userId.
* Shared by assertWorkoutOwned, assertAuthProviderOwned, and
* assertExerciseLibraryOwned.
*/
async function assertOwnedBy(
conn: Client,
table: string,
id: string,
userId: string,
notFoundMessage: string,
forbiddenMessage: string
) {
const row = await conn.execute({
sql: `SELECT userId FROM ${table} WHERE id = ?`,
args: [id]
});
if (row.rows.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: notFoundMessage });
}
if (row.rows[0].userId !== userId) {
throw new TRPCError({ code: "FORBIDDEN", message: forbiddenMessage });
}
}
/** Assert that the workout identified by workoutId is owned by userId */
export async function assertWorkoutOwned(
conn: Client,
workoutId: string,
userId: string
) {
const row = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [workoutId]
});
if (row.rows.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "Workout not found" });
}
if ((row.rows[0] as any).userId !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Not the workout owner"
});
}
await assertOwnedBy(
conn,
"workouts",
workoutId,
userId,
"Workout not found",
"Not the workout owner"
);
}
/** Assert that the auth provider record identified by providerId is owned by userId */
@@ -34,22 +54,14 @@ export async function assertAuthProviderOwned(
providerId: string,
userId: string
) {
const row = await conn.execute({
sql: "SELECT userId FROM authProviders WHERE id = ?",
args: [providerId]
});
if (row.rows.length === 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Auth provider not found"
});
}
if ((row.rows[0] as any).userId !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Not the auth provider owner"
});
}
await assertOwnedBy(
conn,
"authProviders",
providerId,
userId,
"Auth provider not found",
"Not the auth provider owner"
);
}
/** Assert that the exercise library record identified by exerciseId is owned by userId */
@@ -58,19 +70,14 @@ export async function assertExerciseLibraryOwned(
exerciseId: string,
userId: string
) {
const row = await conn.execute({
sql: "SELECT userId FROM exerciseLibrary WHERE id = ?",
args: [exerciseId]
});
if (row.rows.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "Exercise not found" });
}
if ((row.rows[0] as any).userId !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Not the exercise owner"
});
}
await assertOwnedBy(
conn,
"exerciseLibrary",
exerciseId,
userId,
"Exercise not found",
"Not the exercise owner"
);
}
const paginatedQuerySchema = z.object({
@@ -239,6 +246,401 @@ const bulkSchema = z.object({
authProviders: z.array(providerSchema).optional()
});
async function upsertUsers(
conn: Client,
userId: string,
users: z.infer<typeof userInputSchema>[]
) {
for (const user of users) {
if (user.id !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, appleUserId, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET email = excluded.email, emailVerified = excluded.emailVerified, firstName = excluded.firstName, lastName = excluded.lastName, displayName = excluded.displayName, avatarUrl = excluded.avatarUrl, provider = excluded.provider, appleUserId = excluded.appleUserId, status = excluded.status, updatedAt = datetime('now')`,
args: [
user.id,
user.email ?? null,
user.emailVerified ?? 0,
user.firstName ?? null,
user.lastName ?? null,
user.displayName ?? null,
user.avatarUrl ?? null,
user.provider ?? null,
user.appleUserId ?? null,
user.status ?? "active"
]
});
}
}
export async function upsertExerciseLibrary(
conn: Client,
userId: string,
exerciseLibrary: z.infer<typeof exerciseLibrarySchema>[]
) {
for (const exercise of exerciseLibrary) {
if (exercise.userId !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO exerciseLibrary (id, userId, name, category, muscleGroups, equipment, instructions, defaultSets, defaultReps, defaultRestSeconds, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET userId = excluded.userId, name = excluded.name, category = excluded.category, muscleGroups = excluded.muscleGroups, equipment = excluded.equipment, instructions = excluded.instructions, defaultSets = excluded.defaultSets, defaultReps = excluded.defaultReps, defaultRestSeconds = excluded.defaultRestSeconds, notes = excluded.notes, updatedAt = datetime('now')`,
args: [
exercise.id,
exercise.userId,
exercise.name,
exercise.category,
exercise.muscleGroups ?? null,
exercise.equipment ?? null,
exercise.instructions ?? null,
exercise.defaultSets ?? null,
exercise.defaultReps ?? null,
exercise.defaultRestSeconds ?? null,
exercise.notes ?? null
]
});
}
}
async function upsertWorkoutPlans(
conn: Client,
userId: string,
workoutPlans: z.infer<typeof workoutPlanSchema>[]
) {
for (const plan of workoutPlans) {
if (plan.userId !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workoutPlans (id, userId, name, description, category, difficulty, durationMinutes, type, isPublic)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, category = excluded.category, difficulty = excluded.difficulty, durationMinutes = excluded.durationMinutes, type = excluded.type, isPublic = excluded.isPublic, updatedAt = datetime('now')`,
args: [
plan.id,
plan.userId,
plan.name,
plan.description ?? null,
plan.category,
plan.difficulty ?? "intermediate",
plan.durationMinutes ?? null,
plan.type,
plan.isPublic ?? 0
]
});
}
}
async function upsertPlanExercises(
conn: Client,
userId: string,
planExercises: z.infer<typeof planExerciseSchema>[]
) {
for (const planExercise of planExercises) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [planExercise.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== userId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO planExercises (id, planId, exerciseId, name, category, orderIndex, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET exerciseId = excluded.exerciseId, name = excluded.name, category = excluded.category, orderIndex = excluded.orderIndex, notes = excluded.notes`,
args: [
planExercise.id,
planExercise.planId,
planExercise.exerciseId ?? null,
planExercise.name,
planExercise.category,
planExercise.orderIndex,
planExercise.notes ?? null
]
});
}
}
async function upsertPlanSets(
conn: Client,
userId: string,
planSets: z.infer<typeof planSetSchema>[]
) {
for (const planSet of planSets) {
const planExerciseCheck = await conn.execute({
sql: "SELECT planId FROM planExercises WHERE id = ?",
args: [planSet.planExerciseId]
});
if (planExerciseCheck.rows.length) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [planExerciseCheck.rows[0].planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== userId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
}
await conn.execute({
sql: `INSERT INTO planSets (id, planExerciseId, setNumber, reps, weight, durationSeconds, rpe, restAfterSeconds, isWarmup, isDropset, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET setNumber = excluded.setNumber, reps = excluded.reps, weight = excluded.weight, durationSeconds = excluded.durationSeconds, rpe = excluded.rpe, restAfterSeconds = excluded.restAfterSeconds, isWarmup = excluded.isWarmup, isDropset = excluded.isDropset, notes = excluded.notes`,
args: [
planSet.id,
planSet.planExerciseId,
planSet.setNumber,
planSet.reps ?? null,
planSet.weight ?? null,
planSet.durationSeconds ?? null,
planSet.rpe ?? null,
planSet.restAfterSeconds ?? null,
planSet.isWarmup ?? 0,
planSet.isDropset ?? 0,
planSet.notes ?? null
]
});
}
}
async function upsertRoutePoints(
conn: Client,
userId: string,
routePoints: z.infer<typeof routePointSchema>[]
) {
for (const point of routePoints) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [point.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== userId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO routePoints (id, planId, latitude, longitude, orderIndex, isWaypoint)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET latitude = excluded.latitude, longitude = excluded.longitude, orderIndex = excluded.orderIndex, isWaypoint = excluded.isWaypoint`,
args: [
point.id,
point.planId,
point.latitude,
point.longitude,
point.orderIndex,
point.isWaypoint ?? 0
]
});
}
}
async function upsertWorkouts(
conn: Client,
userId: string,
workouts: z.infer<typeof workoutSchema>[]
) {
for (const workout of workouts) {
if (workout.userId !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workouts (id, userId, planId, type, name, startDate, endDate, durationSeconds, distanceMeters, calories, averageHeartRate, maxHeartRate, averagePace, elevationGain, status, source, healthKitUUID, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET planId = excluded.planId, type = excluded.type, name = excluded.name, startDate = excluded.startDate, endDate = excluded.endDate, durationSeconds = excluded.durationSeconds, distanceMeters = excluded.distanceMeters, calories = excluded.calories, averageHeartRate = excluded.averageHeartRate, maxHeartRate = excluded.maxHeartRate, averagePace = excluded.averagePace, elevationGain = excluded.elevationGain, status = excluded.status, source = excluded.source, healthKitUUID = excluded.healthKitUUID, notes = excluded.notes, updatedAt = datetime('now')`,
args: [
workout.id,
workout.userId,
workout.planId ?? null,
workout.type,
workout.name ?? null,
workout.startDate,
workout.endDate ?? null,
workout.durationSeconds ?? null,
workout.distanceMeters ?? null,
workout.calories ?? null,
workout.averageHeartRate ?? null,
workout.maxHeartRate ?? null,
workout.averagePace ?? null,
workout.elevationGain ?? null,
workout.status,
workout.source,
workout.healthKitUUID ?? null,
workout.notes ?? null
]
});
}
}
async function upsertHeartRateSamples(
conn: Client,
userId: string,
heartRateSamples: z.infer<typeof heartRateSchema>[]
) {
for (const sample of heartRateSamples) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [sample.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== userId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO heartRateSamples (id, workoutId, timestamp, bpm, source)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET timestamp = excluded.timestamp, bpm = excluded.bpm, source = excluded.source`,
args: [
sample.id,
sample.workoutId,
sample.timestamp,
sample.bpm,
sample.source ?? null
]
});
}
}
async function upsertLocationSamples(
conn: Client,
userId: string,
locationSamples: z.infer<typeof locationSampleSchema>[]
) {
for (const sample of locationSamples) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [sample.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== userId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO locationSamples (id, workoutId, timestamp, latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, speed, course)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET timestamp = excluded.timestamp, latitude = excluded.latitude, longitude = excluded.longitude, altitude = excluded.altitude, horizontalAccuracy = excluded.horizontalAccuracy, verticalAccuracy = excluded.verticalAccuracy, speed = excluded.speed, course = excluded.course`,
args: [
sample.id,
sample.workoutId,
sample.timestamp,
sample.latitude,
sample.longitude,
sample.altitude ?? null,
sample.horizontalAccuracy ?? null,
sample.verticalAccuracy ?? null,
sample.speed ?? null,
sample.course ?? null
]
});
}
}
async function upsertWorkoutSplits(
conn: Client,
userId: string,
workoutSplits: z.infer<typeof workoutSplitSchema>[]
) {
for (const split of workoutSplits) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [split.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== userId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workoutSplits (id, workoutId, splitNumber, distanceMeters, durationSeconds, startTimestamp, endTimestamp, averageHeartRate, averagePace, elevationGain, elevationLoss)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET splitNumber = excluded.splitNumber, distanceMeters = excluded.distanceMeters, durationSeconds = excluded.durationSeconds, startTimestamp = excluded.startTimestamp, endTimestamp = excluded.endTimestamp, averageHeartRate = excluded.averageHeartRate, averagePace = excluded.averagePace, elevationGain = excluded.elevationGain, elevationLoss = excluded.elevationLoss`,
args: [
split.id,
split.workoutId,
split.splitNumber,
split.distanceMeters,
split.durationSeconds,
split.startTimestamp,
split.endTimestamp,
split.averageHeartRate ?? null,
split.averagePace ?? null,
split.elevationGain ?? null,
split.elevationLoss ?? null
]
});
}
}
async function upsertAuthProviders(
conn: Client,
userId: string,
authProviders: z.infer<typeof providerSchema>[]
) {
for (const provider of authProviders) {
if (provider.userId !== userId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET provider = excluded.provider, providerUserId = excluded.providerUserId, email = excluded.email, displayName = excluded.displayName, avatarUrl = excluded.avatarUrl, lastUsedAt = datetime('now')`,
args: [
provider.id,
provider.userId,
provider.provider,
provider.providerUserId ?? null,
provider.email ?? null,
provider.displayName ?? null,
provider.avatarUrl ?? null
]
});
}
}
export const nessaDbRouter = createTRPCRouter({
health: nessaProcedure.query(async () => {
try {
@@ -1826,356 +2228,41 @@ export const nessaDbRouter = createTRPCRouter({
try {
const conn = NessaConnectionFactory();
if (input.users?.length) {
for (const user of input.users) {
if (user.id !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO users (id, email, emailVerified, firstName, lastName, displayName, avatarUrl, provider, appleUserId, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET email = excluded.email, emailVerified = excluded.emailVerified, firstName = excluded.firstName, lastName = excluded.lastName, displayName = excluded.displayName, avatarUrl = excluded.avatarUrl, provider = excluded.provider, appleUserId = excluded.appleUserId, status = excluded.status, updatedAt = datetime('now')`,
args: [
user.id,
user.email ?? null,
user.emailVerified ?? 0,
user.firstName ?? null,
user.lastName ?? null,
user.displayName ?? null,
user.avatarUrl ?? null,
user.provider ?? null,
user.appleUserId ?? null,
user.status ?? "active"
]
});
}
}
if (input.exerciseLibrary?.length) {
for (const exercise of input.exerciseLibrary) {
if (exercise.userId !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO exerciseLibrary (id, userId, name, category, muscleGroups, equipment, instructions, defaultSets, defaultReps, defaultRestSeconds, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET userId = excluded.userId, name = excluded.name, category = excluded.category, muscleGroups = excluded.muscleGroups, equipment = excluded.equipment, instructions = excluded.instructions, defaultSets = excluded.defaultSets, defaultReps = excluded.defaultReps, defaultRestSeconds = excluded.defaultRestSeconds, notes = excluded.notes, updatedAt = datetime('now')`,
args: [
exercise.id,
exercise.userId,
exercise.name,
exercise.category,
exercise.muscleGroups ?? null,
exercise.equipment ?? null,
exercise.instructions ?? null,
exercise.defaultSets ?? null,
exercise.defaultReps ?? null,
exercise.defaultRestSeconds ?? null,
exercise.notes ?? null
]
});
}
}
if (input.workoutPlans?.length) {
for (const plan of input.workoutPlans) {
if (plan.userId !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workoutPlans (id, userId, name, description, category, difficulty, durationMinutes, type, isPublic)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, category = excluded.category, difficulty = excluded.difficulty, durationMinutes = excluded.durationMinutes, type = excluded.type, isPublic = excluded.isPublic, updatedAt = datetime('now')`,
args: [
plan.id,
plan.userId,
plan.name,
plan.description ?? null,
plan.category,
plan.difficulty ?? "intermediate",
plan.durationMinutes ?? null,
plan.type,
plan.isPublic ?? 0
]
});
}
}
if (input.planExercises?.length) {
for (const planExercise of input.planExercises) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [planExercise.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO planExercises (id, planId, exerciseId, name, category, orderIndex, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET exerciseId = excluded.exerciseId, name = excluded.name, category = excluded.category, orderIndex = excluded.orderIndex, notes = excluded.notes`,
args: [
planExercise.id,
planExercise.planId,
planExercise.exerciseId ?? null,
planExercise.name,
planExercise.category,
planExercise.orderIndex,
planExercise.notes ?? null
]
});
}
}
if (input.planSets?.length) {
for (const planSet of input.planSets) {
const planExerciseCheck = await conn.execute({
sql: "SELECT planId FROM planExercises WHERE id = ?",
args: [planSet.planExerciseId]
});
if (planExerciseCheck.rows.length) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [planExerciseCheck.rows[0].planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
}
await conn.execute({
sql: `INSERT INTO planSets (id, planExerciseId, setNumber, reps, weight, durationSeconds, rpe, restAfterSeconds, isWarmup, isDropset, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET setNumber = excluded.setNumber, reps = excluded.reps, weight = excluded.weight, durationSeconds = excluded.durationSeconds, rpe = excluded.rpe, restAfterSeconds = excluded.restAfterSeconds, isWarmup = excluded.isWarmup, isDropset = excluded.isDropset, notes = excluded.notes`,
args: [
planSet.id,
planSet.planExerciseId,
planSet.setNumber,
planSet.reps ?? null,
planSet.weight ?? null,
planSet.durationSeconds ?? null,
planSet.rpe ?? null,
planSet.restAfterSeconds ?? null,
planSet.isWarmup ?? 0,
planSet.isDropset ?? 0,
planSet.notes ?? null
]
});
}
}
if (input.routePoints?.length) {
for (const point of input.routePoints) {
const planCheck = await conn.execute({
sql: "SELECT userId FROM workoutPlans WHERE id = ?",
args: [point.planId]
});
if (
!planCheck.rows.length ||
planCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO routePoints (id, planId, latitude, longitude, orderIndex, isWaypoint)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET latitude = excluded.latitude, longitude = excluded.longitude, orderIndex = excluded.orderIndex, isWaypoint = excluded.isWaypoint`,
args: [
point.id,
point.planId,
point.latitude,
point.longitude,
point.orderIndex,
point.isWaypoint ?? 0
]
});
}
}
if (input.workouts?.length) {
for (const workout of input.workouts) {
if (workout.userId !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workouts (id, userId, planId, type, name, startDate, endDate, durationSeconds, distanceMeters, calories, averageHeartRate, maxHeartRate, averagePace, elevationGain, status, source, healthKitUUID, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET planId = excluded.planId, type = excluded.type, name = excluded.name, startDate = excluded.startDate, endDate = excluded.endDate, durationSeconds = excluded.durationSeconds, distanceMeters = excluded.distanceMeters, calories = excluded.calories, averageHeartRate = excluded.averageHeartRate, maxHeartRate = excluded.maxHeartRate, averagePace = excluded.averagePace, elevationGain = excluded.elevationGain, status = excluded.status, source = excluded.source, healthKitUUID = excluded.healthKitUUID, notes = excluded.notes, updatedAt = datetime('now')`,
args: [
workout.id,
workout.userId,
workout.planId ?? null,
workout.type,
workout.name ?? null,
workout.startDate,
workout.endDate ?? null,
workout.durationSeconds ?? null,
workout.distanceMeters ?? null,
workout.calories ?? null,
workout.averageHeartRate ?? null,
workout.maxHeartRate ?? null,
workout.averagePace ?? null,
workout.elevationGain ?? null,
workout.status,
workout.source,
workout.healthKitUUID ?? null,
workout.notes ?? null
]
});
}
}
if (input.heartRateSamples?.length) {
for (const sample of input.heartRateSamples) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [sample.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO heartRateSamples (id, workoutId, timestamp, bpm, source)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET timestamp = excluded.timestamp, bpm = excluded.bpm, source = excluded.source`,
args: [
sample.id,
sample.workoutId,
sample.timestamp,
sample.bpm,
sample.source ?? null
]
});
}
}
if (input.locationSamples?.length) {
for (const sample of input.locationSamples) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [sample.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO locationSamples (id, workoutId, timestamp, latitude, longitude, altitude, horizontalAccuracy, verticalAccuracy, speed, course)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET timestamp = excluded.timestamp, latitude = excluded.latitude, longitude = excluded.longitude, altitude = excluded.altitude, horizontalAccuracy = excluded.horizontalAccuracy, verticalAccuracy = excluded.verticalAccuracy, speed = excluded.speed, course = excluded.course`,
args: [
sample.id,
sample.workoutId,
sample.timestamp,
sample.latitude,
sample.longitude,
sample.altitude ?? null,
sample.horizontalAccuracy ?? null,
sample.verticalAccuracy ?? null,
sample.speed ?? null,
sample.course ?? null
]
});
}
}
if (input.workoutSplits?.length) {
for (const split of input.workoutSplits) {
const workoutCheck = await conn.execute({
sql: "SELECT userId FROM workouts WHERE id = ?",
args: [split.workoutId]
});
if (
!workoutCheck.rows.length ||
workoutCheck.rows[0].userId !== ctx.nessaUserId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO workoutSplits (id, workoutId, splitNumber, distanceMeters, durationSeconds, startTimestamp, endTimestamp, averageHeartRate, averagePace, elevationGain, elevationLoss)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET splitNumber = excluded.splitNumber, distanceMeters = excluded.distanceMeters, durationSeconds = excluded.durationSeconds, startTimestamp = excluded.startTimestamp, endTimestamp = excluded.endTimestamp, averageHeartRate = excluded.averageHeartRate, averagePace = excluded.averagePace, elevationGain = excluded.elevationGain, elevationLoss = excluded.elevationLoss`,
args: [
split.id,
split.workoutId,
split.splitNumber,
split.distanceMeters,
split.durationSeconds,
split.startTimestamp,
split.endTimestamp,
split.averageHeartRate ?? null,
split.averagePace ?? null,
split.elevationGain ?? null,
split.elevationLoss ?? null
]
});
}
}
if (input.authProviders?.length) {
for (const provider of input.authProviders) {
if (provider.userId !== ctx.nessaUserId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User mismatch"
});
}
await conn.execute({
sql: `INSERT INTO authProviders (id, userId, provider, providerUserId, email, displayName, avatarUrl)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET provider = excluded.provider, providerUserId = excluded.providerUserId, email = excluded.email, displayName = excluded.displayName, avatarUrl = excluded.avatarUrl, lastUsedAt = datetime('now')`,
args: [
provider.id,
provider.userId,
provider.provider,
provider.providerUserId ?? null,
provider.email ?? null,
provider.displayName ?? null,
provider.avatarUrl ?? null
]
});
}
}
await upsertUsers(conn, ctx.nessaUserId, input.users ?? []);
await upsertExerciseLibrary(
conn,
ctx.nessaUserId,
input.exerciseLibrary ?? []
);
await upsertWorkoutPlans(conn, ctx.nessaUserId, input.workoutPlans ?? []);
await upsertPlanExercises(
conn,
ctx.nessaUserId,
input.planExercises ?? []
);
await upsertPlanSets(conn, ctx.nessaUserId, input.planSets ?? []);
await upsertRoutePoints(conn, ctx.nessaUserId, input.routePoints ?? []);
await upsertWorkouts(conn, ctx.nessaUserId, input.workouts ?? []);
await upsertHeartRateSamples(
conn,
ctx.nessaUserId,
input.heartRateSamples ?? []
);
await upsertLocationSamples(
conn,
ctx.nessaUserId,
input.locationSamples ?? []
);
await upsertWorkoutSplits(
conn,
ctx.nessaUserId,
input.workoutSplits ?? []
);
await upsertAuthProviders(
conn,
ctx.nessaUserId,
input.authProviders ?? []
);
return { success: true };
} catch (error) {

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
import { TRPCError } from "@trpc/server";
import diff from "fast-diff";
export function createDiffPatch(
function createDiffPatch(
oldContent: string,
newContent: string
): string {
@@ -12,7 +12,7 @@ export function createDiffPatch(
return JSON.stringify(changes);
}
export function applyDiffPatch(baseContent: string, patchJson: string): string {
function applyDiffPatch(baseContent: string, patchJson: string): string {
const changes = JSON.parse(patchJson);
let result = "";
let position = 0;
@@ -96,7 +96,6 @@ export const postHistoryRouter = createTRPCRouter({
const conn = ConnectionFactory();
// Verify post exists and user is author
const postCheck = await conn.execute({
sql: "SELECT author_id FROM Post WHERE id = ?",
args: [input.postId]

View File

@@ -254,7 +254,6 @@ export const userRouter = createTRPCRouter({
args: [passwordHash, userId]
});
// Send email notification about password being set
if (user.email) {
try {
const h3Event = ctx.event.nativeEvent

View File

@@ -6,105 +6,6 @@ import { z } from "zod";
* Schemas for post creation, updating, querying, and interactions
*/
// ============================================================================
// Post Category and Status
// ============================================================================
/**
* Post category enum (deprecated but kept for backward compatibility)
*/
export const postCategorySchema = z.enum(["blog", "project"]);
// ============================================================================
// Post Creation and Updates
// ============================================================================
/**
* Create new post schema
*/
export const createPostSchema = z.object({
title: z
.string()
.min(1, "Title is required")
.max(200, "Title must be under 200 characters"),
subtitle: z
.string()
.max(300, "Subtitle must be under 300 characters")
.optional(),
body: z.string().min(1, "Post body is required"),
banner_photo: z.string().url("Must be a valid URL").optional(),
published: z.boolean().default(false),
category: postCategorySchema.default("blog"),
attachments: z.string().optional()
});
/**
* Update post schema (partial updates)
*/
export const updatePostSchema = z.object({
postId: z.number(),
title: z.string().min(1).max(200).optional(),
subtitle: z.string().max(300).optional(),
body: z.string().min(1).optional(),
banner_photo: z.string().url().optional(),
published: z.boolean().optional(),
attachments: z.string().optional()
});
/**
* Delete post schema
*/
export const deletePostSchema = z.object({
postId: z.number()
});
// ============================================================================
// Post Queries and Filtering
// ============================================================================
/**
* Post sort mode enum
* Defines available sorting options for blog posts
*/
export const postSortModeSchema = z.enum([
"newest",
"oldest",
"most_liked",
"most_read",
"most_comments"
]);
/**
* Post query input schema
* Accepts optional filters (pipe-separated tags) and sort mode
*/
export const postQueryInputSchema = z.object({
/**
* Pipe-separated list of tags to filter by
* e.g., "tech|design|javascript"
* Empty string or undefined means no filter
*/
filters: z.string().optional(),
/**
* Sort mode for posts
* Defaults to "newest" if not specified
*/
sortBy: postSortModeSchema.default("newest")
});
/**
* Get single post by ID or slug
*/
export const getPostSchema = z
.object({
postId: z.number().optional(),
slug: z.string().optional()
})
.refine((data) => data.postId || data.slug, {
message: "Either postId or slug must be provided"
});
// ============================================================================
// Post Interactions
// ============================================================================
@@ -116,55 +17,8 @@ export const incrementPostReadSchema = z.object({
postId: z.number()
});
/**
* Like/unlike post
*/
export const togglePostLikeSchema = z.object({
postId: z.number()
});
// ============================================================================
// Tag Management
// ============================================================================
/**
* Add tags to post
*/
export const addTagsToPostSchema = z.object({
postId: z.number(),
tags: z
.array(z.string().min(1).max(50))
.min(1, "At least one tag is required")
});
/**
* Remove tag from post
*/
export const removeTagFromPostSchema = z.object({
tagId: z.number()
});
/**
* Update post tags (replaces all tags)
*/
export const updatePostTagsSchema = z.object({
postId: z.number(),
tags: z.array(z.string().min(1).max(50))
});
// ============================================================================
// Type Exports
// ============================================================================
export type PostCategory = z.infer<typeof postCategorySchema>;
export type CreatePostInput = z.infer<typeof createPostSchema>;
export type UpdatePostInput = z.infer<typeof updatePostSchema>;
export type DeletePostInput = z.infer<typeof deletePostSchema>;
export type PostSortMode = z.infer<typeof postSortModeSchema>;
export type PostQueryInput = z.infer<typeof postQueryInputSchema>;
export type GetPostInput = z.infer<typeof getPostSchema>;
export type IncrementPostReadInput = z.infer<typeof incrementPostReadSchema>;
export type TogglePostLikeInput = z.infer<typeof togglePostLikeSchema>;
export type AddTagsToPostInput = z.infer<typeof addTagsToPostSchema>;
export type RemoveTagFromPostInput = z.infer<typeof removeTagFromPostSchema>;
export type UpdatePostTagsInput = z.infer<typeof updatePostTagsSchema>;

View File

@@ -1,116 +0,0 @@
/**
* Comment API Validation Schemas
*
* Zod schemas for comment-related tRPC procedures:
* - Comment creation, updating, deletion
* - Comment reactions
* - Comment sorting and filtering
*/
import { z } from "zod";
// ============================================================================
// Comment CRUD Operations
// ============================================================================
/**
* Create new comment schema
*/
export const createCommentSchema = z.object({
body: z
.string()
.min(1, "Comment cannot be empty")
.max(5000, "Comment too long"),
post_id: z.number(),
parent_comment_id: z.number().optional()
});
/**
* Update comment schema
*/
export const updateCommentSchema = z.object({
commentId: z.number(),
body: z
.string()
.min(1, "Comment cannot be empty")
.max(5000, "Comment too long")
});
/**
* Delete comment schema
*/
export const deleteCommentSchema = z.object({
commentId: z.number(),
deletionType: z.enum(["user", "admin", "database"]).optional()
});
/**
* Get comments for post schema
*/
export const getCommentsSchema = z.object({
postId: z.number(),
sortBy: z.enum(["newest", "oldest", "highest_rated", "hot"]).default("newest")
});
// ============================================================================
// Comment Reactions
// ============================================================================
/**
* Valid reaction types
*/
export const reactionTypeSchema = z.enum([
"tears",
"blank",
"tongue",
"cry",
"heartEye",
"angry",
"moneyEye",
"sick",
"upsideDown",
"worried"
]);
/**
* Add/remove reaction to comment
*/
export const toggleCommentReactionSchema = z.object({
commentId: z.number(),
reactionType: reactionTypeSchema
});
/**
* Get reactions for comment
*/
export const getCommentReactionsSchema = z.object({
commentId: z.number()
});
// ============================================================================
// Comment Sorting
// ============================================================================
/**
* Valid comment sorting modes
*/
export const commentSortSchema = z
.enum(["newest", "oldest", "highest_rated", "hot"])
.default("newest");
// ============================================================================
// Type Exports
// ============================================================================
export type CommentSortMode = z.infer<typeof commentSortSchema>;
export type ReactionType = z.infer<typeof reactionTypeSchema>;
export type CreateCommentInput = z.infer<typeof createCommentSchema>;
export type UpdateCommentInput = z.infer<typeof updateCommentSchema>;
export type DeleteCommentInput = z.infer<typeof deleteCommentSchema>;
export type GetCommentsInput = z.infer<typeof getCommentsSchema>;
export type ToggleCommentReactionInput = z.infer<
typeof toggleCommentReactionSchema
>;
export type GetCommentReactionsInput = z.infer<
typeof getCommentReactionsSchema
>;

View File

@@ -7,71 +7,10 @@ import { z } from "zod";
* Use these schemas for validating database inputs and outputs in tRPC procedures
*/
// ============================================================================
// User Schemas
// ============================================================================
/**
* Full User schema matching database structure
*/
export const userSchema = z.object({
id: z.string(),
email: z.string().email().nullable().optional(),
email_verified: z.number(),
password_hash: z.string().nullable().optional(),
display_name: z.string().nullable().optional(),
provider: z.enum(["email", "google", "github"]).nullable().optional(),
image: z.string().url().nullable().optional(),
apple_user_string: z.string().nullable().optional(),
database_name: z.string().nullable().optional(),
database_token: z.string().nullable().optional(),
database_url: z.string().nullable().optional(),
db_destroy_date: z.string().nullable().optional(),
created_at: z.string(),
updated_at: z.string()
});
/**
* User creation input (for registration)
*/
export const createUserSchema = z.object({
email: z.string().email().optional(),
password: z.string().min(8).optional(),
display_name: z.string().min(1).max(50).optional(),
provider: z.enum(["email", "google", "github"]).optional(),
image: z.string().url().optional()
});
/**
* User update input (partial updates)
*/
export const updateUserSchema = z.object({
email: z.string().email().optional(),
display_name: z.string().min(1).max(50).optional(),
image: z.string().url().optional()
});
// ============================================================================
// Post Schemas
// ============================================================================
/**
* Full Post schema matching database structure
*/
export const postSchema = z.object({
id: z.number(),
category: z.enum(["blog", "project"]),
title: z.string(),
subtitle: z.string().optional(),
body: z.string(),
banner_photo: z.string().optional(),
date: z.string(),
published: z.boolean(),
author_id: z.string(),
reads: z.number(),
attachments: z.string().optional()
});
/**
* Post creation input
*/
@@ -97,47 +36,6 @@ export const updatePostSchema = z.object({
attachments: z.string().optional()
});
/**
* Post with aggregated data
*/
export const postWithCommentsAndLikesSchema = postSchema.extend({
total_likes: z.number(),
total_comments: z.number()
});
// ============================================================================
// Comment Schemas
// ============================================================================
/**
* Full Comment schema matching database structure
*/
export const commentSchema = z.object({
id: z.number(),
body: z.string(),
post_id: z.number(),
parent_comment_id: z.number().optional(),
date: z.string(),
edited: z.boolean(),
commenter_id: z.string()
});
/**
* Comment creation input
*/
export const createCommentSchema = z.object({
body: z.string().min(1).max(5000),
post_id: z.number(),
parent_comment_id: z.number().optional()
});
/**
* Comment update input
*/
export const updateCommentSchema = z.object({
body: z.string().min(1).max(5000)
});
// ============================================================================
// CommentReaction Schemas
// ============================================================================
@@ -160,94 +58,6 @@ export const reactionTypeSchema = z.enum([
"downVote"
]);
/**
* Full CommentReaction schema matching database structure
*/
export const commentReactionSchema = z.object({
id: z.number(),
type: reactionTypeSchema,
comment_id: z.number(),
user_id: z.string()
});
/**
* Comment reaction creation input
*/
export const createCommentReactionSchema = z.object({
type: reactionTypeSchema,
comment_id: z.number()
});
// ============================================================================
// PostLike Schemas
// ============================================================================
/**
* Full PostLike schema matching database structure
*/
export const postLikeSchema = z.object({
id: z.number(),
user_id: z.string(),
post_id: z.number()
});
/**
* PostLike creation input
*/
export const createPostLikeSchema = z.object({
post_id: z.number()
});
// ============================================================================
// Tag Schemas
// ============================================================================
/**
* Full Tag schema matching database structure
*/
export const tagSchema = z.object({
id: z.number(),
value: z.string(),
post_id: z.number()
});
/**
* Tag creation input
*/
export const createTagSchema = z.object({
value: z.string().min(1).max(50),
post_id: z.number()
});
/**
* PostWithTags schema
*/
export const postWithTagsSchema = postSchema.extend({
tags: z.array(tagSchema)
});
// ============================================================================
// Connection Schemas
// ============================================================================
/**
* Full Connection schema matching database structure
*/
export const connectionSchema = z.object({
id: z.number(),
user_id: z.string(),
connection_id: z.string(),
post_id: z.number().optional()
});
/**
* Connection creation input
*/
export const createConnectionSchema = z.object({
connection_id: z.string(),
post_id: z.number().optional()
});
// ============================================================================
// Common Query Schemas
// ============================================================================
@@ -259,26 +69,6 @@ export const idSchema = z.object({
id: z.number()
});
export const userIdSchema = z.object({
userId: z.string()
});
export const postIdSchema = z.object({
postId: z.number()
});
export const commentIdSchema = z.object({
commentId: z.number()
});
/**
* Pagination schema
*/
export const paginationSchema = z.object({
limit: z.number().min(1).max(100).default(10),
offset: z.number().min(0).default(0)
});
// ============================================================================
// Additional Database Router Schemas
// ============================================================================
@@ -343,10 +133,6 @@ export const getUserByIdSchema = z.object({
id: z.string()
});
export const getUserPublicDataSchema = z.object({
id: z.string()
});
export const updateUserImageSchema = z.object({
id: z.string(),
imageURL: z.string()
@@ -365,15 +151,6 @@ export const updateUserEmailSchema = z.object({
export type ReactionType = z.infer<typeof reactionTypeSchema>;
export type CreatePostInput = z.infer<typeof createPostSchema>;
export type UpdatePostInput = z.infer<typeof updatePostSchema>;
export type CreateCommentInput = z.infer<typeof createCommentSchema>;
export type UpdateCommentInput = z.infer<typeof updateCommentSchema>;
export type CreateCommentReactionInput = z.infer<
typeof createCommentReactionSchema
>;
export type CreatePostLikeInput = z.infer<typeof createPostLikeSchema>;
export type CreateTagInput = z.infer<typeof createTagSchema>;
export type CreateConnectionInput = z.infer<typeof createConnectionSchema>;
export type PaginationInput = z.infer<typeof paginationSchema>;
export type GetPostByIdInput = z.infer<typeof getPostByIdSchema>;
export type GetPostByTitleInput = z.infer<typeof getPostByTitleSchema>;
export type GetCommentsByPostIdInput = z.infer<

View File

@@ -65,11 +65,6 @@ export const loginUserSchema = z.object({
rememberMe: z.boolean().optional().default(false)
});
/**
* OAuth provider schema
*/
export const oauthProviderSchema = z.enum(["google", "github"]);
// ============================================================================
// Profile Management Schemas
// ============================================================================
@@ -168,20 +163,12 @@ export const deleteAccountSchema = z.object({
password: z.string().min(1, "Password is required to delete account")
});
/**
* Email verification schema
*/
export const verifyEmailSchema = z.object({
token: z.string().min(1)
});
// ============================================================================
// Type Exports
// ============================================================================
export type RegisterUserInput = z.infer<typeof registerUserSchema>;
export type LoginUserInput = z.infer<typeof loginUserSchema>;
export type OAuthProvider = z.infer<typeof oauthProviderSchema>;
export type UpdateEmailInput = z.infer<typeof updateEmailSchema>;
export type UpdateDisplayNameInput = z.infer<typeof updateDisplayNameSchema>;
export type UpdateProfileImageInput = z.infer<typeof updateProfileImageSchema>;
@@ -192,4 +179,3 @@ export type RequestPasswordResetInput = z.infer<
>;
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
export type DeleteAccountInput = z.infer<typeof deleteAccountSchema>;
export type VerifyEmailInput = z.infer<typeof verifyEmailSchema>;

View File

@@ -89,7 +89,6 @@ describe("Audit Logging System", () => {
});
it("should not throw errors on logging failures", async () => {
// This should not throw even if there's an invalid event type
await expect(
logAuditEvent({
eventType: "invalid.test.event",
@@ -101,7 +100,6 @@ describe("Audit Logging System", () => {
describe("queryAuditLogs", () => {
beforeEach(async () => {
// Create test logs
await logAuditEvent({
eventType: "auth.login.success",
eventData: { test: "test-query-1", testUser: "user-1" },
@@ -199,7 +197,6 @@ describe("Audit Logging System", () => {
describe("getFailedLoginAttempts", () => {
beforeEach(async () => {
// Create failed login attempts
for (let i = 0; i < 5; i++) {
await logAuditEvent({
eventType: "auth.login.failed",
@@ -212,7 +209,6 @@ describe("Audit Logging System", () => {
});
}
// Create successful logins (should be excluded)
await logAuditEvent({
eventType: "auth.login.success",
eventData: { test: "test-success-1" },
@@ -241,7 +237,6 @@ describe("Audit Logging System", () => {
const attemptsIn1h = await getFailedLoginAttempts(1, 100);
expect(attemptsIn24h.length).toBeGreaterThanOrEqual(5);
// Recent attempts should be within 1 hour
expect(attemptsIn1h.length).toBeGreaterThanOrEqual(5);
});
});
@@ -302,7 +297,6 @@ describe("Audit Logging System", () => {
describe("detectSuspiciousActivity", () => {
beforeEach(async () => {
// Create suspicious pattern: many failed logins from same IP
for (let i = 0; i < 10; i++) {
await logAuditEvent({
eventType: "auth.login.failed",
@@ -315,7 +309,6 @@ describe("Audit Logging System", () => {
});
}
// Create normal activity
await logAuditEvent({
eventType: "auth.login.success",
eventData: { test: "test-normal-1" },
@@ -344,7 +337,6 @@ describe("Audit Logging System", () => {
it("should return empty array when no suspicious activity", async () => {
await cleanupTestLogs();
// Create only successful logins
await logAuditEvent({
eventType: "auth.login.success",
eventData: { test: "test-clean-1" },
@@ -378,7 +370,6 @@ describe("Audit Logging System", () => {
]
});
// Clean up logs older than 90 days
const deleted = await cleanupOldLogs(90);
expect(deleted).toBeGreaterThanOrEqual(1);
@@ -399,7 +390,6 @@ describe("Audit Logging System", () => {
const logsAfter = await queryAuditLogs({ limit: 100 });
// Should still have recent logs
expect(logsAfter.length).toBeGreaterThan(0);
});
});

View File

@@ -405,7 +405,6 @@ export async function detectSuspiciousActivity(
const currentIp = currentIpOrMinAttempts as string;
const reasons: string[] = [];
// Check for excessive failed logins
const failedAttempts = (await getFailedLoginAttempts(
userId,
"user_id",
@@ -415,7 +414,6 @@ export async function detectSuspiciousActivity(
reasons.push(`${failedAttempts} failed login attempts in last 15 minutes`);
}
// Check for rapid location changes (different IPs in short time)
const recentIps = await conn.execute({
sql: `SELECT DISTINCT ip_address FROM AuditLog
WHERE user_id = ?
@@ -431,7 +429,6 @@ export async function detectSuspiciousActivity(
);
}
// Check for new IP if user has login history
const ipHistory = await conn.execute({
sql: `SELECT COUNT(*) as count FROM AuditLog
WHERE user_id = ?

View File

@@ -100,7 +100,6 @@ export async function withCacheAndStale<T>(
const now = Date.now();
const entry = store.get(key) as CacheEntry<T> | undefined;
// Fresh hit
if (entry && entry.expiresAt > now) return entry.data;
try {
@@ -116,7 +115,6 @@ export async function withCacheAndStale<T>(
console.error(`Error fetching data for cache key "${key}":`, error);
}
// Stale fallback
if (entry && entry.staleExpiresAt > now) {
if (logErrors) console.log(`Serving stale data for cache key "${key}"`);
return entry.data;

View File

@@ -254,7 +254,6 @@ describe("Clerk user.created webhook", () => {
describe("Clerk user.updated webhook", () => {
it("updates mutable fields and leaves clerkUserId unchanged", async () => {
// seed via created
await call(sign(userCreatedPayload()));
const before = getUserByClerkId("user_abc123");

View File

@@ -1,6 +1,3 @@
import type { H3Event } from "vinxi/http";
import { UAParser } from "ua-parser-js";
export interface DeviceInfo {
deviceName?: string;
deviceType?: "desktop" | "mobile" | "tablet";
@@ -8,61 +5,6 @@ export interface DeviceInfo {
os?: string;
}
/**
* Parse user agent string to extract device information
* @param userAgent - User agent string from request headers
* @returns Parsed device information
*/
export function parseDeviceInfo(userAgent: string): DeviceInfo {
const parser = new UAParser(userAgent);
const result = parser.getResult();
// Determine device type
let deviceType: "desktop" | "mobile" | "tablet" = "desktop";
if (result.device.type === "mobile") {
deviceType = "mobile";
} else if (result.device.type === "tablet") {
deviceType = "tablet";
}
// Build device name (e.g., "iPhone 14", "Windows PC", "iPad Pro")
let deviceName: string | undefined;
if (result.device.vendor && result.device.model) {
deviceName = `${result.device.vendor} ${result.device.model}`;
} else if (result.os.name) {
deviceName = `${result.os.name} ${deviceType === "desktop" ? "Computer" : deviceType}`;
}
// Browser info (e.g., "Chrome 120")
const browser =
result.browser.name && result.browser.version
? `${result.browser.name} ${result.browser.version.split(".")[0]}`
: result.browser.name;
// OS info (e.g., "macOS 14.1", "Windows 11", "iOS 17")
const os =
result.os.name && result.os.version
? `${result.os.name} ${result.os.version}`
: result.os.name;
return {
deviceName,
deviceType,
browser,
os
};
}
/**
* Extract device information from H3Event
* @param event - H3Event
* @returns Device information
*/
export function getDeviceInfo(event: H3Event): DeviceInfo {
const userAgent = event.node.req.headers["user-agent"] || "";
return parseDeviceInfo(userAgent);
}
/**
* Generate a human-readable device description
* @param deviceInfo - Device information
@@ -85,18 +27,3 @@ export function formatDeviceDescription(deviceInfo: DeviceInfo): string {
return parts.length > 0 ? parts.join(" • ") : "Unknown Device";
}
/**
* Create a short device fingerprint for comparison
* Not cryptographic, just for grouping similar logins
* @param deviceInfo - Device information
* @returns Short fingerprint string
*/
export function createDeviceFingerprint(deviceInfo: DeviceInfo): string {
const parts = [
deviceInfo.deviceType || "unknown",
deviceInfo.os?.split(" ")[0] || "unknown",
deviceInfo.browser?.split(" ")[0] || "unknown"
];
return parts.join("-").toLowerCase();
}

View File

@@ -5,7 +5,6 @@ import loginLinkTemplate from "./login-link.html?raw";
import passwordResetTemplate from "./password-reset.html?raw";
import emailVerificationTemplate from "./email-verification.html?raw";
import providerLinkedTemplate from "./provider-linked.html?raw";
import newDeviceLoginTemplate from "./new-device-login.html?raw";
import passwordSetTemplate from "./password-set.html?raw";
/**
@@ -119,29 +118,6 @@ export function generateProviderLinkedEmail(
});
}
export interface NewDeviceLoginEmailParams {
deviceInfo: string;
loginTime: string;
ipAddress: string;
loginMethod: string;
accountUrl: string;
}
/**
* Generate new device login notification email HTML
*/
export function generateNewDeviceLoginEmail(
params: NewDeviceLoginEmailParams
): string {
return processTemplate(newDeviceLoginTemplate, {
DEVICE_INFO: params.deviceInfo,
LOGIN_TIME: params.loginTime,
IP_ADDRESS: params.ipAddress,
LOGIN_METHOD: params.loginMethod,
ACCOUNT_URL: params.accountUrl
});
}
export interface PasswordSetEmailParams {
providerName: string;
setTime: string;

View File

@@ -12,7 +12,6 @@ import {
async function testTimeoutError() {
console.log("\n=== Testing Timeout Error ===");
try {
// This should timeout after 1ms
await fetchWithTimeout("https://httpbin.org/delay/10", { timeout: 1 });
console.log("❌ Should have thrown TimeoutError");
} catch (error) {
@@ -29,7 +28,6 @@ async function testTimeoutError() {
async function testNetworkError() {
console.log("\n=== Testing Network Error ===");
try {
// This should fail to connect
await fetchWithTimeout(
"https://invalid-domain-that-does-not-exist-12345.com"
);
@@ -47,7 +45,6 @@ async function testNetworkError() {
async function testAPIError() {
console.log("\n=== Testing API Error ===");
try {
// This should return 404
const response = await fetchWithTimeout("https://httpbin.org/status/404");
await checkResponse(response);
console.log("❌ Should have thrown APIError");

View File

@@ -1,15 +0,0 @@
import { defineMiddleware, setHeaders } from "vinxi/http";
// Security headers middleware — sets CSP and hardening headers on all responses
export default defineMiddleware({
onRequest: (event) => {
setHeaders(event, {
"Content-Security-Policy":
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()"
});
}
});

View File

@@ -57,7 +57,6 @@ describe("verifyNessaToken with Clerk JWT", () => {
expect(result.exp).toBe(mockPayload.exp);
expect(result.iat).toBe(mockPayload.iat);
// Verify verifyToken was called with correct options
expect(mockVerifyToken).toHaveBeenCalledWith(
"valid-clerk-session-token",
expect.objectContaining({

View File

@@ -33,8 +33,6 @@ export async function verifyNessaToken(
): Promise<NessaAuthPayload> {
const payload = await verifyToken(token, {
secretKey: env.NESSA_CLERK_SECRET,
// Optional: restrict to specific issuers / apps
// audience: env.NESSA_CLERK_JWT_ISSUER,
});
if (!payload.sub) {

View File

@@ -34,7 +34,6 @@ export async function linkProvider(
): Promise<UserProvider> {
const conn = ConnectionFactory();
// Check if provider already linked to this user
const existing = await conn.execute({
sql: "SELECT * FROM UserProvider WHERE user_id = ? AND provider = ?",
args: [userId, provider]
@@ -44,7 +43,6 @@ export async function linkProvider(
throw new Error(`Provider ${provider} already linked to this account`);
}
// Check if provider identity is already used by another user
if (providerData.providerUserId) {
const conflictCheck = await conn.execute({
sql: "SELECT user_id FROM UserProvider WHERE provider = ? AND provider_user_id = ?",
@@ -61,7 +59,6 @@ export async function linkProvider(
}
}
// Create new provider link
const id = uuidV4();
await conn.execute({
sql: `INSERT INTO UserProvider (id, user_id, provider, provider_user_id, email, display_name, image)
@@ -77,7 +74,6 @@ export async function linkProvider(
]
});
// Fetch created record
const result = await conn.execute({
sql: "SELECT * FROM UserProvider WHERE id = ?",
args: [id]
@@ -85,7 +81,6 @@ export async function linkProvider(
const userProvider = result.rows[0] as unknown as UserProvider;
// Log audit event
await logAuditEvent({
userId,
eventType: "auth.provider.linked",
@@ -99,7 +94,6 @@ export async function linkProvider(
// Send notification email if requested and user has email
if (options?.sendEmail !== false) {
try {
// Get user email
const userResult = await conn.execute({
sql: "SELECT email FROM User WHERE id = ?",
args: [userId]
@@ -150,7 +144,6 @@ export async function unlinkProvider(
): Promise<void> {
const conn = ConnectionFactory();
// Check how many providers this user has
const providersResult = await conn.execute({
sql: "SELECT COUNT(*) as count FROM UserProvider WHERE user_id = ?",
args: [userId]
@@ -164,7 +157,6 @@ export async function unlinkProvider(
);
}
// Delete provider
const result = await conn.execute({
sql: "DELETE FROM UserProvider WHERE user_id = ? AND provider = ?",
args: [userId, provider]
@@ -174,7 +166,6 @@ export async function unlinkProvider(
throw new Error(`Provider ${provider} not found for this user`);
}
// Log audit event
await logAuditEvent({
userId,
eventType: "auth.provider.unlinked",
@@ -261,7 +252,6 @@ export async function findUserByProviderEmail(
export async function findUserByEmail(email: string): Promise<string | null> {
const conn = ConnectionFactory();
// First check User table
const userResult = await conn.execute({
sql: "SELECT id FROM User WHERE email = ?",
args: [email]
@@ -271,7 +261,6 @@ export async function findUserByEmail(email: string): Promise<string | null> {
return (userResult.rows[0] as any).id;
}
// Then check UserProvider table
const providerResult = await conn.execute({
sql: "SELECT user_id FROM UserProvider WHERE email = ? LIMIT 1",
args: [email]

View File

@@ -21,7 +21,6 @@ describe("CSRF Protection", () => {
const token = generateCSRFToken();
expect(token).toBeDefined();
expect(typeof token).toBe("string");
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
expect(token).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
@@ -34,7 +33,6 @@ describe("CSRF Protection", () => {
});
it("should generate cryptographically secure tokens", () => {
// Generate multiple tokens and ensure no collisions
const tokens = new Set<string>();
for (let i = 0; i < 1000; i++) {
tokens.add(generateCSRFToken());
@@ -50,7 +48,6 @@ describe("CSRF Protection", () => {
expect(token).toBeDefined();
expect(typeof token).toBe("string");
// Token should be a UUID
expect(token).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
@@ -122,7 +119,6 @@ describe("CSRF Protection", () => {
const invalidToken1 = "b".repeat(36);
const invalidToken2 = "b".repeat(35) + "a";
// Test timing for completely different tokens
const event1 = createMockEvent({
headers: { "x-csrf-token": invalidToken1 },
cookies: { "csrf-token": validToken }
@@ -132,7 +128,6 @@ describe("CSRF Protection", () => {
validateCSRFToken(event1);
const time1 = performance.now() - start1;
// Test timing for tokens that differ only at the end
const event2 = createMockEvent({
headers: { "x-csrf-token": invalidToken2 },
cookies: { "csrf-token": validToken }
@@ -142,7 +137,6 @@ describe("CSRF Protection", () => {
validateCSRFToken(event2);
const time2 = performance.now() - start2;
// Timing difference should be minimal (less than 1ms)
// This tests for constant-time comparison
const timeDiff = Math.abs(time1 - time2);
expect(timeDiff).toBeLessThan(1);
@@ -161,7 +155,6 @@ describe("CSRF Protection", () => {
describe("CSRF Attack Scenarios", () => {
it("should prevent basic CSRF attack", () => {
// Attacker doesn't have access to the CSRF token cookie
const attackEvent = createMockEvent({
headers: { "x-csrf-token": "attacker-guessed-token" }
});
@@ -174,7 +167,6 @@ describe("CSRF Protection", () => {
const token1 = generateCSRFToken();
const token2 = generateCSRFToken();
// User has token1, attacker tries to use token2
const event = createMockEvent({
headers: { "x-csrf-token": token2 },
cookies: { "csrf-token": token1 }
@@ -198,7 +190,6 @@ describe("CSRF Protection", () => {
});
it("should prevent replay attacks with old tokens", () => {
// Simulate an old token that was captured
const oldToken = "old-captured-token-12345";
const event = createMockEvent({
@@ -206,10 +197,8 @@ describe("CSRF Protection", () => {
cookies: { "csrf-token": oldToken }
});
// Even if tokens match, they should be validated by the system
// This test validates the structure works correctly
const isValid = validateCSRFToken(event);
expect(isValid).toBe(true); // Matches are valid
expect(isValid).toBe(true);
});
});
@@ -270,9 +259,7 @@ describe("CSRF Protection", () => {
tokens.push(generateCSRFToken());
}
// Check for sequential patterns
for (let i = 1; i < tokens.length; i++) {
// Tokens should not be incrementing
expect(tokens[i]).not.toBe(
String(Number(tokens[i - 1].replace(/-/g, "")) + 1)
);
@@ -281,11 +268,9 @@ describe("CSRF Protection", () => {
it("should generate tokens with sufficient entropy", () => {
const token = generateCSRFToken();
// UUID without dashes should be 32 hex characters
const hexString = token.replace(/-/g, "");
expect(hexString).toMatch(/^[0-9a-f]{32}$/i);
// Check that not all characters are the same
const uniqueChars = new Set(hexString.split(""));
expect(uniqueChars.size).toBeGreaterThan(5);
});
@@ -299,7 +284,6 @@ describe("CSRF Protection", () => {
}
const duration = performance.now() - start;
// Should generate 1000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
@@ -316,7 +300,6 @@ describe("CSRF Protection", () => {
}
const duration = performance.now() - start;
// Should validate 10000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
});
@@ -402,7 +385,6 @@ describe("CSRF Protection", () => {
const sessionAToken = generateCSRFToken();
const sessionBToken = generateCSRFToken();
// Session A's cookie with Session B's header token
const event = createMockEvent({
headers: { "x-csrf-token": sessionBToken },
cookies: { "csrf-token": sessionAToken }
@@ -522,12 +504,10 @@ describe("CSRF Protection", () => {
it("should issue CSRF token on setCSRFToken then validate it", () => {
const event = createMockEvent({});
// Step 1: Login issues CSRF token
const token = setCSRFToken(event);
expect(token).toBeDefined();
expect(typeof token).toBe("string");
// Step 2: Subsequent mutation sends token back
const mutationEvent = createMockEvent({
headers: { "x-csrf-token": token },
cookies: { "csrf-token": token }
@@ -538,7 +518,6 @@ describe("CSRF Protection", () => {
});
it("should reject cross-origin POST without CSRF token", () => {
// Simulated cross-site POST: attacker can read cookies but not set headers
const attackEvent = createMockEvent({
// No x-csrf-token header (cross-origin requests can't set custom headers)
cookies: { "csrf-token": "victim-token" }

View File

@@ -59,7 +59,6 @@ describe("Input Validation and Injection Prevention", () => {
for (const email of sqlEmails) {
// Either reject as invalid, or it's properly escaped in queries
const isValid = isValidEmail(email);
// Test documents the behavior
expect(typeof isValid).toBe("boolean");
}
});
@@ -68,7 +67,6 @@ describe("Input Validation and Injection Prevention", () => {
const longEmail = "a".repeat(1000) + "@example.com";
const result = isValidEmail(longEmail);
// Should handle gracefully
expect(typeof result).toBe("boolean");
});
@@ -130,7 +128,6 @@ describe("Input Validation and Injection Prevention", () => {
it("should use parameterized queries for user authentication", async () => {
const conn = ConnectionFactory();
// Test that SQL injection attempts don't work
const maliciousEmail = "admin'--";
try {
@@ -140,7 +137,6 @@ describe("Input Validation and Injection Prevention", () => {
args: [maliciousEmail]
});
// Should return no results (no user with that exact email)
expect(result.rows.length).toBe(0);
} catch (error) {
// If error, ensure it's not a SQL error
@@ -153,7 +149,6 @@ describe("Input Validation and Injection Prevention", () => {
for (const payload of SQL_INJECTION_PAYLOADS) {
try {
// Test various injection points
await conn.execute({
sql: "SELECT * FROM User WHERE email = ?",
args: [payload]
@@ -164,7 +159,6 @@ describe("Input Validation and Injection Prevention", () => {
args: [payload]
});
// Queries should complete without SQL errors
expect(true).toBe(true);
} catch (error: any) {
// If error occurs, should not be SQL injection syntax error
@@ -184,10 +178,8 @@ describe("Input Validation and Injection Prevention", () => {
args: [unionPayload]
});
// Should not return password hashes
if (result.rows.length > 0) {
for (const row of result.rows) {
// Ensure we don't get password_hash column
expect(row).not.toHaveProperty("password_hash");
}
}
@@ -200,7 +192,6 @@ describe("Input Validation and Injection Prevention", () => {
it("should prevent blind SQL injection timing attacks", async () => {
const conn = ConnectionFactory();
// Timing-based payload
const timingPayload = "admin' AND SLEEP(5)--";
const start = performance.now();
@@ -210,18 +201,15 @@ describe("Input Validation and Injection Prevention", () => {
args: [timingPayload]
});
} catch (error) {
// Ignore errors
}
const duration = performance.now() - start;
// Should not delay for 5 seconds
expect(duration).toBeLessThan(1000);
});
it("should prevent second-order SQL injection", async () => {
const conn = ConnectionFactory();
// Store malicious data
const maliciousName = "admin'--";
try {
@@ -236,7 +224,6 @@ describe("Input Validation and Injection Prevention", () => {
]
});
// Retrieve and use (should still be safe with parameterized queries)
const result = await conn.execute({
sql: "SELECT display_name FROM User WHERE email = ?",
args: ["test-sqli@example.com"]
@@ -244,13 +231,11 @@ describe("Input Validation and Injection Prevention", () => {
expect(result.rows.length).toBeGreaterThanOrEqual(0);
// Cleanup
await conn.execute({
sql: "DELETE FROM User WHERE email = ?",
args: ["test-sqli@example.com"]
});
} catch (error) {
// Should not have SQL syntax errors
expect(error).toBeDefined();
}
});
@@ -260,7 +245,6 @@ describe("Input Validation and Injection Prevention", () => {
it("should identify potentially dangerous XSS patterns", () => {
// These payloads should be handled by frontend sanitization
for (const payload of XSS_PAYLOADS) {
// Document that these patterns exist
expect(payload).toBeDefined();
expect(typeof payload).toBe("string");
@@ -272,11 +256,9 @@ describe("Input Validation and Injection Prevention", () => {
it("should handle script tags in user input", () => {
const scriptInput = "<script>alert('XSS')</script>";
// Validation should not crash
const nameValid = isValidDisplayName(scriptInput);
expect(typeof nameValid).toBe("boolean");
// Email validation
const emailValid = isValidEmail(scriptInput);
expect(typeof emailValid).toBe("boolean");
});
@@ -450,7 +432,6 @@ describe("Input Validation and Injection Prevention", () => {
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
// Should complete quickly (no ReDoS)
expect(duration).toBeLessThan(100);
});
@@ -508,14 +489,12 @@ describe("Input Validation and Injection Prevention", () => {
});
it("should not be vulnerable to ReDoS attacks", () => {
// ReDoS payload with many repetitions
const redosPayload = "a".repeat(1000) + "!";
const start = performance.now();
validatePassword(redosPayload);
const duration = performance.now() - start;
// Should complete quickly
expect(duration).toBeLessThan(100);
});
});

View File

@@ -20,7 +20,6 @@ describe("Password Security", () => {
expect(hash).toBeDefined();
expect(typeof hash).toBe("string");
// Bcrypt hashes start with $2b$ or $2a$
expect(hash).toMatch(/^\$2[ab]\$/);
});
@@ -36,7 +35,6 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Bcrypt hashes are 60 characters long
expect(hash.length).toBe(60);
});
@@ -132,32 +130,26 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Measure time for correct password
const { duration: correctDuration } = await measureTime(() =>
checkPasswordSafe(password, hash)
);
// Measure time for incorrect password
const { duration: incorrectDuration } = await measureTime(() =>
checkPasswordSafe("WrongPassword123!", hash)
);
// Bcrypt comparison should take similar time regardless
const timingDifference = Math.abs(correctDuration - incorrectDuration);
// Allow reasonable variance (bcrypt is inherently slow)
expect(timingDifference).toBeLessThan(50);
});
it("should handle null hash without timing leak", async () => {
const password = "TestPassword123!";
// Measure time for null hash
const { result: result1, duration: duration1 } = await measureTime(() =>
checkPasswordSafe(password, null)
);
// Measure time for undefined hash
const { result: result2, duration: duration2 } = await measureTime(() =>
checkPasswordSafe(password, undefined)
);
@@ -165,7 +157,6 @@ describe("Password Security", () => {
expect(result1).toBe(false);
expect(result2).toBe(false);
// Should take similar time
const timingDifference = Math.abs(duration1 - duration2);
expect(timingDifference).toBeLessThan(50);
});
@@ -178,7 +169,6 @@ describe("Password Security", () => {
checkPasswordSafe(password, null)
);
// Should take at least a few milliseconds (bcrypt is slow)
expect(duration).toBeGreaterThan(1);
});
@@ -186,12 +176,10 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// User exists
const { duration: existsDuration } = await measureTime(() =>
checkPasswordSafe("WrongPassword", hash)
);
// User doesn't exist (null hash)
const { duration: notExistsDuration } = await measureTime(() =>
checkPasswordSafe("WrongPassword", null)
);
@@ -280,9 +268,9 @@ describe("Password Security", () => {
});
it("should calculate password strength correctly", () => {
const fairPassword = "MyP@ssw0rd12"; // 12 chars
const goodPassword = "MyStr0ng!P@ssw0rd"; // 17 chars
const strongPassword = "MyV3ry!Str0ng@P@ssw0rd123"; // 25 chars
const fairPassword = "MyP@ssw0rd12";
const goodPassword = "MyStr0ng!P@ssw0rd";
const strongPassword = "MyV3ry!Str0ng@P@ssw0rd123";
expect(validatePassword(fairPassword).strength).toBe("fair");
expect(validatePassword(goodPassword).strength).toBe("good");
@@ -337,7 +325,6 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Measure time for multiple checks (simulating brute force)
const start = performance.now();
const attempts = 10;
@@ -348,7 +335,6 @@ describe("Password Security", () => {
const duration = performance.now() - start;
const avgPerAttempt = duration / attempts;
// Each attempt should take significant time (bcrypt is slow)
// This makes brute force impractical
expect(avgPerAttempt).toBeGreaterThan(5); // At least 5ms per attempt
});
@@ -356,18 +342,15 @@ describe("Password Security", () => {
it("should prevent rainbow table attacks with unique salts", async () => {
const password = "CommonPassword123!";
// Generate multiple hashes for same password
const hashes = await Promise.all(
Array.from({ length: 10 }, () => hashPassword(password))
);
// All hashes should be unique (different salts)
const uniqueHashes = new Set(hashes);
expect(uniqueHashes.size).toBe(10);
});
it("should prevent password spraying with validation", () => {
// Common passwords that should be rejected
const commonPasswords = [
"Password123!",
"Welcome123!",
@@ -382,7 +365,6 @@ describe("Password Security", () => {
});
it("should resist dictionary attacks", () => {
// Dictionary words that should be caught
const dictionaryBased = ["Sunshine123!", "Princess456!", "Dragon789!@"];
for (const password of dictionaryBased) {
@@ -394,7 +376,7 @@ describe("Password Security", () => {
describe("Edge Cases", () => {
it("should handle very long passwords", async () => {
const longPassword = "A1!a" + "x".repeat(1000); // Very long but valid
const longPassword = "A1!a" + "x".repeat(1000);
const hash = await hashPassword(longPassword);
const match = await checkPassword(longPassword, hash);
@@ -413,7 +395,6 @@ describe("Password Security", () => {
const hash = await hashPassword(nullBytePassword);
const match = await checkPassword(nullBytePassword, hash);
// Behavior may vary - just ensure no crash
expect(typeof match).toBe("boolean");
});
@@ -450,7 +431,6 @@ describe("Password Security", () => {
const duration = performance.now() - start;
// Bcrypt should be slow enough to deter brute force
// With 10 rounds, should take at least a few milliseconds
expect(duration).toBeGreaterThan(5);
// But not too slow for normal operation
expect(duration).toBeLessThan(500);
@@ -467,11 +447,9 @@ describe("Password Security", () => {
durations.push(performance.now() - start);
}
// Timing should be relatively consistent
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
const maxDeviation = Math.max(...durations.map((d) => Math.abs(d - avg)));
// Allow reasonable variance
expect(maxDeviation).toBeLessThan(avg * 0.5);
});
@@ -484,7 +462,6 @@ describe("Password Security", () => {
}
const duration = performance.now() - start;
// Validation is CPU-bound but should be fast
expect(duration).toBeLessThan(100);
});
});
@@ -494,12 +471,9 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Check that hash uses correct salt rounds
// Bcrypt format: $2b$rounds$salthash
const parts = hash.split("$");
const rounds = parseInt(parts[2]);
// Should use 10 rounds (from password.ts)
expect(rounds).toBe(10);
});
@@ -509,17 +483,14 @@ describe("Password Security", () => {
Array.from({ length: 100 }, () => hashPassword(password))
);
// Extract salts from hashes
const salts = hashes.map((hash) => {
const parts = hash.split("$");
return parts[3].substring(0, 22); // Salt is 22 characters
});
// All salts should be unique
const uniqueSalts = new Set(salts);
expect(uniqueSalts.size).toBe(100);
// Check for patterns in salts (should be random)
for (let i = 1; i < salts.length; i++) {
// Salts should not be sequential or predictable
expect(salts[i]).not.toBe(salts[i - 1]);

View File

@@ -60,12 +60,10 @@ describe("Rate Limiting", () => {
const maxAttempts = 3;
const windowMs = 60000;
// Use up all attempts
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(identifier, maxAttempts, windowMs);
}
// Next attempt should throw
try {
await checkRateLimit(identifier, maxAttempts, windowMs);
expect.unreachable("Should have thrown");
@@ -79,7 +77,6 @@ describe("Rate Limiting", () => {
const maxAttempts = 2;
const windowMs = 60000;
// Use up all attempts
await checkRateLimit(identifier, maxAttempts, windowMs);
await checkRateLimit(identifier, maxAttempts, windowMs);
@@ -99,12 +96,10 @@ describe("Rate Limiting", () => {
const maxAttempts = 3;
const windowMs = 500; // 500ms window for testing
// Use up all attempts
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(identifier, maxAttempts, windowMs);
}
// Should be blocked immediately after
try {
await checkRateLimit(identifier, maxAttempts, windowMs);
expect.unreachable("Should have thrown");
@@ -112,10 +107,8 @@ describe("Rate Limiting", () => {
expect(error).toBeInstanceOf(TRPCError);
}
// Wait for window to expire
await new Promise((resolve) => setTimeout(resolve, 600));
// Should be allowed again
const remaining = await checkRateLimit(identifier, maxAttempts, windowMs);
expect(remaining).toBe(maxAttempts - 1);
});
@@ -125,13 +118,11 @@ describe("Rate Limiting", () => {
const maxAttempts = 10;
const windowMs = 60000;
// Simulate concurrent requests
const results: number[] = [];
for (let i = 0; i < maxAttempts; i++) {
results.push(await checkRateLimit(identifier, maxAttempts, windowMs));
}
// All should succeed with decreasing remaining counts
expect(results).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
});
@@ -142,12 +133,10 @@ describe("Rate Limiting", () => {
const id1 = uniqueId("test1");
const id2 = uniqueId("test2");
// Use up attempts for id1
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(id1, maxAttempts, windowMs);
}
// id1 should be blocked
try {
await checkRateLimit(id1, maxAttempts, windowMs);
expect.unreachable("Should have thrown");
@@ -155,7 +144,6 @@ describe("Rate Limiting", () => {
expect(error).toBeInstanceOf(TRPCError);
}
// id2 should still work
const remaining = await checkRateLimit(id2, maxAttempts, windowMs);
expect(remaining).toBe(maxAttempts - 1);
});
@@ -225,12 +213,10 @@ describe("Rate Limiting", () => {
const email = `test-${Date.now()}@example.com`;
// IP rate limiting is skipped in test/dev, so only email limit applies
// Use up email rate limit with same email
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
await rateLimitLogin(email, ip);
}
// Next attempt should fail due to email limit
try {
await rateLimitLogin(email, ip);
expect.unreachable("Should have thrown");
@@ -242,12 +228,10 @@ describe("Rate Limiting", () => {
it("should limit by email independently of IP", async () => {
const email = `test-${Date.now()}@example.com`;
// Use different IPs but same email
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
await rateLimitLogin(email, randomIP());
}
// Next attempt with different IP should still fail due to email limit
try {
await rateLimitLogin(email, randomIP());
expect.unreachable("Should have thrown");
@@ -260,13 +244,11 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// In test/dev, IP rate limiting is skipped
// Should allow many different emails from same IP
for (let i = 0; i < 10; i++) {
const email = `test${i}-${Date.now()}@example.com`;
await rateLimitLogin(email, ip);
}
// Should not throw since IP limits are disabled in test/dev
expect(true).toBe(true);
});
});
@@ -276,12 +258,10 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// IP rate limiting is skipped in test/dev
// Should allow many attempts
for (let i = 0; i < 10; i++) {
await rateLimitPasswordReset(ip);
}
// Should not throw in test/dev
expect(true).toBe(true);
});
@@ -304,12 +284,10 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// IP rate limiting is skipped in test/dev
// Should allow many attempts
for (let i = 0; i < 10; i++) {
await rateLimitRegistration(ip);
}
// Should not throw in test/dev
expect(true).toBe(true);
});
});
@@ -319,12 +297,10 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// IP rate limiting is skipped in test/dev
// Should allow many attempts
for (let i = 0; i < 10; i++) {
await rateLimitEmailVerification(ip);
}
// Should not throw in test/dev
expect(true).toBe(true);
});
});
@@ -334,7 +310,6 @@ describe("Rate Limiting", () => {
const email = "victim@example.com";
const attackerIP = "1.2.3.4";
// Simulate brute force attack
let blockedAtAttempt = 0;
for (let i = 0; i < 10; i++) {
try {
@@ -347,7 +322,6 @@ describe("Rate Limiting", () => {
}
}
// Should be blocked before 10 attempts
expect(blockedAtAttempt).toBeLessThan(10);
expect(blockedAtAttempt).toBeGreaterThan(0);
});
@@ -355,7 +329,6 @@ describe("Rate Limiting", () => {
it("should prevent distributed brute force from multiple IPs", async () => {
const email = "victim@example.com";
// Simulate distributed attack from different IPs
let blockedAtAttempt = 0;
for (let i = 0; i < 10; i++) {
try {
@@ -368,7 +341,6 @@ describe("Rate Limiting", () => {
}
}
// Should be blocked at email limit (3 attempts)
expect(blockedAtAttempt).toBeLessThanOrEqual(
RATE_LIMITS.LOGIN_EMAIL.maxAttempts
);
@@ -383,7 +355,6 @@ describe("Rate Limiting", () => {
await rateLimitRegistration(attackerIP);
}
// Should not block in test/dev (IP limits disabled)
expect(true).toBe(true);
});
@@ -396,7 +367,6 @@ describe("Rate Limiting", () => {
await rateLimitPasswordReset(attackerIP);
}
// Should not block in test/dev (IP limits disabled)
expect(true).toBe(true);
});
});
@@ -408,14 +378,12 @@ describe("Rate Limiting", () => {
const unknownIP = "unknown";
const email = `test-${Date.now()}@example.com`;
// Should allow many login attempts in development with unknown IP
// (only email rate limit applies)
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
const testEmail = `test-${Date.now()}-${i}@example.com`;
await rateLimitLogin(testEmail, unknownIP);
}
// Should be able to continue with different emails (no IP limit in dev)
await rateLimitLogin(`final-${Date.now()}@example.com`, unknownIP);
});
@@ -423,12 +391,10 @@ describe("Rate Limiting", () => {
const unknownIP = "unknown";
const email = `test-${Date.now()}@example.com`;
// Use up email rate limit
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
await rateLimitLogin(email, unknownIP);
}
// Next attempt should fail due to email limit
try {
await rateLimitLogin(email, unknownIP);
expect.unreachable("Should have thrown");
@@ -440,55 +406,46 @@ describe("Rate Limiting", () => {
it("should handle unknown IP in password reset", async () => {
const unknownIP = "unknown";
// In development, should allow many attempts (no IP limit)
for (let i = 0; i < 10; i++) {
await rateLimitPasswordReset(unknownIP);
}
// Should not throw in development
expect(true).toBe(true);
});
it("should handle unknown IP in registration", async () => {
const unknownIP = "unknown";
// In development, should allow many attempts (no IP limit)
for (let i = 0; i < 10; i++) {
await rateLimitRegistration(unknownIP);
}
// Should not throw in development
expect(true).toBe(true);
});
it("should handle unknown IP in email verification", async () => {
const unknownIP = "unknown";
// In development, should allow many attempts (no IP limit)
for (let i = 0; i < 10; i++) {
await rateLimitEmailVerification(unknownIP);
}
// Should not throw in development
expect(true).toBe(true);
});
});
describe("Rate Limit Configuration", () => {
it("should have reasonable limits configured", () => {
// Login should be more permissive than registration
expect(RATE_LIMITS.LOGIN_IP.maxAttempts).toBeGreaterThan(
RATE_LIMITS.REGISTRATION_IP.maxAttempts
);
// All limits should be positive
expect(RATE_LIMITS.LOGIN_IP.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.LOGIN_EMAIL.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.REGISTRATION_IP.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.EMAIL_VERIFICATION_IP.maxAttempts).toBeGreaterThan(0);
// All windows should be at least 1 minute
expect(RATE_LIMITS.LOGIN_IP.windowMs).toBeGreaterThanOrEqual(60000);
expect(RATE_LIMITS.LOGIN_EMAIL.windowMs).toBeGreaterThanOrEqual(60000);
expect(RATE_LIMITS.PASSWORD_RESET_IP.windowMs).toBeGreaterThanOrEqual(
@@ -552,7 +509,6 @@ describe("Rate Limiting", () => {
const maxAttempts = 3;
const windowMs = 60000;
// Exhaust the limit: 3 allowed, 4th blocked.
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(id, maxAttempts, windowMs);
}
@@ -574,7 +530,6 @@ describe("Rate Limiting", () => {
const maxAttempts = 5;
const windowMs = 60000;
// Instance A: 3 attempts.
clearRateLimitLocalCache();
for (let i = 0; i < 3; i++) {
await checkRateLimit(id, maxAttempts, windowMs);
@@ -582,9 +537,9 @@ describe("Rate Limiting", () => {
// Instance B (fresh local cache) makes 2 more -> combined count = 5.
clearRateLimitLocalCache();
await checkRateLimit(id, maxAttempts, windowMs); // count 4
const remaining = await checkRateLimit(id, maxAttempts, windowMs); // count 5
expect(remaining).toBe(0); // 5th allowed, no remaining
await checkRateLimit(id, maxAttempts, windowMs);
const remaining = await checkRateLimit(id, maxAttempts, windowMs);
expect(remaining).toBe(0);
// A 6th attempt from a fresh instance must be blocked — the shared store
// aggregated the count across the two "instances".

View File

@@ -4,8 +4,6 @@
*/
import type { H3Event } from "vinxi/http";
import { SignJWT } from "jose";
import { env } from "~/env/server";
/**
* Create a mock H3Event for testing
@@ -62,55 +60,6 @@ export function createMockEvent(options: {
return mockEvent;
}
/**
* Generate a valid JWT token for testing
*/
export async function createTestJWT(
userId: string,
expiresIn: string = "1h"
): Promise<string> {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
return await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(expiresIn)
.sign(secret);
}
/**
* Generate an expired JWT token for testing
*/
export async function createExpiredJWT(userId: string): Promise<string> {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
return await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("-1h") // Expired 1 hour ago
.sign(secret);
}
/**
* Generate a JWT with invalid signature
*/
export async function createInvalidSignatureJWT(
userId: string
): Promise<string> {
const wrongSecret = new TextEncoder().encode("wrong-secret-key");
return await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(wrongSecret);
}
/**
* Generate test credentials
*/
export function createTestCredentials() {
return {
email: `test-${Date.now()}@example.com`,
password: "TestPass123!@#",
passwordConfirmation: "TestPass123!@#"
};
}
/**
* Common SQL injection payloads
*/
@@ -138,26 +87,6 @@ export const XSS_PAYLOADS = [
"<input onfocus=alert('XSS') autofocus>"
];
/**
* Wait for async operations with timeout
*/
export async function waitFor(
condition: () => boolean | Promise<boolean>,
timeout: number = 5000,
interval: number = 100
): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await condition()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error(`Timeout waiting for condition after ${timeout}ms`);
}
/**
* Measure execution time
*/
@@ -170,18 +99,6 @@ export async function measureTime<T>(
return { result, duration };
}
/**
* Generate random string for testing
*/
export function randomString(length: number = 10): string {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(
{ length },
() => chars[Math.floor(Math.random() * chars.length)]
).join("");
}
/**
* Generate random IP address
*/