forked from Mike/freno-dev
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:
@@ -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")
|
||||
) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user