diff --git a/.env.example b/.env.example index f8b6a21..6d06e74 100644 --- a/.env.example +++ b/.env.example @@ -11,8 +11,6 @@ # variable below. Do not leave production values blank. # ────────────────────────────────────────────────────────────────────────── -NODE_ENV="development" - # ── Frontend / public (safe to expose to the browser, VITE_* is shipped) ── VITE_DOMAIN="http://localhost:3000" VITE_AWS_BUCKET_STRING="https://example-bucket.s3.amazonaws.com/" @@ -20,10 +18,6 @@ VITE_DOWNLOAD_BUCKET_STRING="example-downloads-bucket" VITE_GOOGLE_CLIENT_ID=".apps.googleusercontent.com" VITE_GOOGLE_CLIENT_ID_DEV=".apps.googleusercontent.com" VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE=".apps.googleusercontent.com" -# Server-side Google client ID for verifying Google ID tokens from the Nessa -# iOS app via verifyIdToken({ audience }). MUST match the iOS app's -# GID_CLIENT_ID in Nessa/Resources/GoogleSignIn.xcconfig. -GOOGLE_CLIENT_ID=".apps.googleusercontent.com" VITE_GITHUB_CLIENT_ID="" VITE_GITHUB_CLIENT_ID_DEV="" VITE_INFILL_ENDPOINT="https://infill.example.com/infill" @@ -52,7 +46,6 @@ GOOGLE_CLIENT_SECRET="" # GOCSPX-... GOOGLE_CLIENT_SECRET_DEV="" GITHUB_CLIENT_SECRET="" GITHUB_CLIENT_SECRET_DEV="" -APPLE_SHARED_SECRET="" # App Store Server Notifications # ── Cloudflare Turnstile ── TURNSTILE_SECRET_KEY="" # 0x... @@ -65,6 +58,9 @@ TURSO_LINEAGE_URL="libsql://.turso.io" TURSO_LINEAGE_TOKEN="" NESSA_DB_URL="libsql://.turso.io" NESSA_DB_TOKEN="" +NESSA_GOOGLE_CLIENT_ID=".apps.googleusercontent.com" + +APPLE_CLIENT_ID="" # ── Infra / integration tokens ── INFILL_BEARER_TOKEN="" diff --git a/.gitignore b/.gitignore index 7db022d..b7391b5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ app.config.timestamp_*.js # Environment .env .env*.local +.env.bak # dependencies /node_modules diff --git a/src/env/server.ts b/src/env/server.ts index ad568ab..879a5a0 100644 --- a/src/env/server.ts +++ b/src/env/server.ts @@ -57,10 +57,9 @@ const serverEnvSchema = z.object({ NESSA_DB_URL: z.string().min(1), NESSA_DB_TOKEN: z.string().min(1), NESSA_JWT_SECRET: z.string().min(1), - // p8-005: dedicated Lineage game JWT signing secret, isolated from the - // web JWT_SECRET_KEY so a web admin secret cannot mint Lineage tokens. LINEAGE_JWT_SECRET: z.string().min(32), - APPLE_CLIENT_ID: z.string().min(1).optional(), + APPLE_CLIENT_ID_NESSA: z.string().min(1).optional(), + APPLE_CLIENT_ID_LINEAGE: z.string().min(1).optional(), VITE_TURNSTILE_SITE_KEY: z.string().min(1), TURNSTILE_SECRET_KEY: z.string().min(1) }); diff --git a/src/server/api/routers/lineage/auth.ts b/src/server/api/routers/lineage/auth.ts index a5e03db..b25090e 100644 --- a/src/server/api/routers/lineage/auth.ts +++ b/src/server/api/routers/lineage/auth.ts @@ -426,8 +426,8 @@ export const lineageAuthRouter = createTRPCRouter({ algorithms: ["RS256"], issuer: "https://appleid.apple.com" }; - if (env.APPLE_CLIENT_ID) { - jwtOptions.audience = env.APPLE_CLIENT_ID; + if (env.APPLE_CLIENT_ID_LINEAGE) { + jwtOptions.audience = env.APPLE_CLIENT_ID_LINEAGE; } const { payload: tokenPayload } = await jwtVerify( input.idToken, diff --git a/src/server/api/routers/nessa.ts b/src/server/api/routers/nessa.ts index ca6a977..6e618a9 100644 --- a/src/server/api/routers/nessa.ts +++ b/src/server/api/routers/nessa.ts @@ -26,7 +26,10 @@ export async function assertWorkoutOwned( 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" }); + throw new TRPCError({ + code: "FORBIDDEN", + message: "Not the workout owner" + }); } } @@ -41,10 +44,16 @@ export async function assertAuthProviderOwned( args: [providerId] }); if (row.rows.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Auth provider not found" }); + 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" }); + throw new TRPCError({ + code: "FORBIDDEN", + message: "Not the auth provider owner" + }); } } @@ -62,7 +71,10 @@ export async function assertExerciseLibraryOwned( 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" }); + throw new TRPCError({ + code: "FORBIDDEN", + message: "Not the exercise owner" + }); } } @@ -708,8 +720,8 @@ export const nessaDbRouter = createTRPCRouter({ algorithms: ["RS256"], issuer: "https://appleid.apple.com" }; - if (env.APPLE_CLIENT_ID) { - jwtOptions.audience = env.APPLE_CLIENT_ID; + if (env.APPLE_CLIENT_ID_NESSA) { + jwtOptions.audience = env.APPLE_CLIENT_ID_NESSA; } const { payload: tokenPayload } = await jwtVerify( input.idToken, @@ -2005,9 +2017,16 @@ export const nessaDbRouter = createTRPCRouter({ args: [input.id] }); if (sample.rows.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Heart rate sample not found" }); + throw new TRPCError({ + code: "NOT_FOUND", + message: "Heart rate sample not found" + }); } - await assertWorkoutOwned(conn, (sample.rows[0] as any).workoutId, ctx.nessaUserId); + await assertWorkoutOwned( + conn, + (sample.rows[0] as any).workoutId, + ctx.nessaUserId + ); await conn.execute({ sql: `UPDATE heartRateSamples SET timestamp = ?, bpm = ?, source = ? WHERE id = ?`, args: [input.timestamp, input.bpm, input.source ?? null, input.id] @@ -2032,9 +2051,16 @@ export const nessaDbRouter = createTRPCRouter({ args: [input.id] }); if (sample.rows.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Heart rate sample not found" }); + throw new TRPCError({ + code: "NOT_FOUND", + message: "Heart rate sample not found" + }); } - await assertWorkoutOwned(conn, (sample.rows[0] as any).workoutId, ctx.nessaUserId); + await assertWorkoutOwned( + conn, + (sample.rows[0] as any).workoutId, + ctx.nessaUserId + ); await conn.execute({ sql: "DELETE FROM heartRateSamples WHERE id = ?", args: [input.id] @@ -2091,9 +2117,16 @@ export const nessaDbRouter = createTRPCRouter({ args: [input.id] }); if (sample.rows.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Location sample not found" }); + throw new TRPCError({ + code: "NOT_FOUND", + message: "Location sample not found" + }); } - await assertWorkoutOwned(conn, (sample.rows[0] as any).workoutId, ctx.nessaUserId); + await assertWorkoutOwned( + conn, + (sample.rows[0] as any).workoutId, + ctx.nessaUserId + ); await conn.execute({ sql: `UPDATE locationSamples SET timestamp = ?, latitude = ?, longitude = ?, altitude = ?, horizontalAccuracy = ?, verticalAccuracy = ?, speed = ?, course = ? WHERE id = ?`, args: [ @@ -2128,9 +2161,16 @@ export const nessaDbRouter = createTRPCRouter({ args: [input.id] }); if (sample.rows.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Location sample not found" }); + throw new TRPCError({ + code: "NOT_FOUND", + message: "Location sample not found" + }); } - await assertWorkoutOwned(conn, (sample.rows[0] as any).workoutId, ctx.nessaUserId); + await assertWorkoutOwned( + conn, + (sample.rows[0] as any).workoutId, + ctx.nessaUserId + ); await conn.execute({ sql: "DELETE FROM locationSamples WHERE id = ?", args: [input.id] @@ -2188,9 +2228,16 @@ export const nessaDbRouter = createTRPCRouter({ args: [input.id] }); if (split.rows.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Workout split not found" }); + throw new TRPCError({ + code: "NOT_FOUND", + message: "Workout split not found" + }); } - await assertWorkoutOwned(conn, (split.rows[0] as any).workoutId, ctx.nessaUserId); + await assertWorkoutOwned( + conn, + (split.rows[0] as any).workoutId, + ctx.nessaUserId + ); await conn.execute({ sql: `UPDATE workoutSplits SET splitNumber = ?, distanceMeters = ?, durationSeconds = ?, startTimestamp = ?, endTimestamp = ?, averageHeartRate = ?, averagePace = ?, elevationGain = ?, elevationLoss = ? WHERE id = ?`, args: [ @@ -2226,9 +2273,16 @@ export const nessaDbRouter = createTRPCRouter({ args: [input.id] }); if (split.rows.length === 0) { - throw new TRPCError({ code: "NOT_FOUND", message: "Workout split not found" }); + throw new TRPCError({ + code: "NOT_FOUND", + message: "Workout split not found" + }); } - await assertWorkoutOwned(conn, (split.rows[0] as any).workoutId, ctx.nessaUserId); + await assertWorkoutOwned( + conn, + (split.rows[0] as any).workoutId, + ctx.nessaUserId + ); await conn.execute({ sql: "DELETE FROM workoutSplits WHERE id = ?", args: [input.id]