fix: env cleanup, updates for new apps

This commit is contained in:
2026-07-22 23:56:10 -04:00
parent 42757bc93d
commit d4621b6ae2
5 changed files with 80 additions and 30 deletions

View File

@@ -11,8 +11,6 @@
# variable below. Do not leave production values blank. # variable below. Do not leave production values blank.
# ────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
NODE_ENV="development"
# ── Frontend / public (safe to expose to the browser, VITE_* is shipped) ── # ── Frontend / public (safe to expose to the browser, VITE_* is shipped) ──
VITE_DOMAIN="http://localhost:3000" VITE_DOMAIN="http://localhost:3000"
VITE_AWS_BUCKET_STRING="https://example-bucket.s3.amazonaws.com/" 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="<google-oauth-client-id>.apps.googleusercontent.com" VITE_GOOGLE_CLIENT_ID="<google-oauth-client-id>.apps.googleusercontent.com"
VITE_GOOGLE_CLIENT_ID_DEV="<google-oauth-client-id-dev>.apps.googleusercontent.com" VITE_GOOGLE_CLIENT_ID_DEV="<google-oauth-client-id-dev>.apps.googleusercontent.com"
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE="<google-oauth-client-id-magicdelve>.apps.googleusercontent.com" VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE="<google-oauth-client-id-magicdelve>.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="<google-oauth-client-id-ios>.apps.googleusercontent.com"
VITE_GITHUB_CLIENT_ID="<github-oauth-client-id>" VITE_GITHUB_CLIENT_ID="<github-oauth-client-id>"
VITE_GITHUB_CLIENT_ID_DEV="<github-oauth-client-id-dev>" VITE_GITHUB_CLIENT_ID_DEV="<github-oauth-client-id-dev>"
VITE_INFILL_ENDPOINT="https://infill.example.com/infill" VITE_INFILL_ENDPOINT="https://infill.example.com/infill"
@@ -52,7 +46,6 @@ GOOGLE_CLIENT_SECRET="<rotate-in-google-cloud-console>" # GOCSPX-...
GOOGLE_CLIENT_SECRET_DEV="<rotate-in-google-cloud-console>" GOOGLE_CLIENT_SECRET_DEV="<rotate-in-google-cloud-console>"
GITHUB_CLIENT_SECRET="<rotate-in-github-oauth-apps>" GITHUB_CLIENT_SECRET="<rotate-in-github-oauth-apps>"
GITHUB_CLIENT_SECRET_DEV="<rotate-in-github-oauth-apps>" GITHUB_CLIENT_SECRET_DEV="<rotate-in-github-oauth-apps>"
APPLE_SHARED_SECRET="<rotate-in-app-developer-portal>" # App Store Server Notifications
# ── Cloudflare Turnstile ── # ── Cloudflare Turnstile ──
TURNSTILE_SECRET_KEY="<rotate-in-cloudflare-dashboard>" # 0x... TURNSTILE_SECRET_KEY="<rotate-in-cloudflare-dashboard>" # 0x...
@@ -65,6 +58,9 @@ TURSO_LINEAGE_URL="libsql://<lineage-db>.turso.io"
TURSO_LINEAGE_TOKEN="<rotate-in-turso-dashboard>" TURSO_LINEAGE_TOKEN="<rotate-in-turso-dashboard>"
NESSA_DB_URL="libsql://<nessa-db>.turso.io" NESSA_DB_URL="libsql://<nessa-db>.turso.io"
NESSA_DB_TOKEN="<rotate-in-turso-dashboard>" NESSA_DB_TOKEN="<rotate-in-turso-dashboard>"
NESSA_GOOGLE_CLIENT_ID="<google-oauth-client-id-ios>.apps.googleusercontent.com"
APPLE_CLIENT_ID="<services-id-for-nessa>"
# ── Infra / integration tokens ── # ── Infra / integration tokens ──
INFILL_BEARER_TOKEN="<rotate-at-infill-service>" INFILL_BEARER_TOKEN="<rotate-at-infill-service>"

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ app.config.timestamp_*.js
# Environment # Environment
.env .env
.env*.local .env*.local
.env.bak
# dependencies # dependencies
/node_modules /node_modules

5
src/env/server.ts vendored
View File

@@ -57,10 +57,9 @@ const serverEnvSchema = z.object({
NESSA_DB_URL: z.string().min(1), NESSA_DB_URL: z.string().min(1),
NESSA_DB_TOKEN: z.string().min(1), NESSA_DB_TOKEN: z.string().min(1),
NESSA_JWT_SECRET: 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), 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), VITE_TURNSTILE_SITE_KEY: z.string().min(1),
TURNSTILE_SECRET_KEY: z.string().min(1) TURNSTILE_SECRET_KEY: z.string().min(1)
}); });

View File

@@ -426,8 +426,8 @@ export const lineageAuthRouter = createTRPCRouter({
algorithms: ["RS256"], algorithms: ["RS256"],
issuer: "https://appleid.apple.com" issuer: "https://appleid.apple.com"
}; };
if (env.APPLE_CLIENT_ID) { if (env.APPLE_CLIENT_ID_LINEAGE) {
jwtOptions.audience = env.APPLE_CLIENT_ID; jwtOptions.audience = env.APPLE_CLIENT_ID_LINEAGE;
} }
const { payload: tokenPayload } = await jwtVerify( const { payload: tokenPayload } = await jwtVerify(
input.idToken, input.idToken,

View File

@@ -26,7 +26,10 @@ export async function assertWorkoutOwned(
throw new TRPCError({ code: "NOT_FOUND", message: "Workout not found" }); throw new TRPCError({ code: "NOT_FOUND", message: "Workout not found" });
} }
if ((row.rows[0] as any).userId !== userId) { 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] args: [providerId]
}); });
if (row.rows.length === 0) { 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) { 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" }); throw new TRPCError({ code: "NOT_FOUND", message: "Exercise not found" });
} }
if ((row.rows[0] as any).userId !== userId) { 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"], algorithms: ["RS256"],
issuer: "https://appleid.apple.com" issuer: "https://appleid.apple.com"
}; };
if (env.APPLE_CLIENT_ID) { if (env.APPLE_CLIENT_ID_NESSA) {
jwtOptions.audience = env.APPLE_CLIENT_ID; jwtOptions.audience = env.APPLE_CLIENT_ID_NESSA;
} }
const { payload: tokenPayload } = await jwtVerify( const { payload: tokenPayload } = await jwtVerify(
input.idToken, input.idToken,
@@ -2005,9 +2017,16 @@ export const nessaDbRouter = createTRPCRouter({
args: [input.id] args: [input.id]
}); });
if (sample.rows.length === 0) { 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({ await conn.execute({
sql: `UPDATE heartRateSamples SET timestamp = ?, bpm = ?, source = ? WHERE id = ?`, sql: `UPDATE heartRateSamples SET timestamp = ?, bpm = ?, source = ? WHERE id = ?`,
args: [input.timestamp, input.bpm, input.source ?? null, input.id] args: [input.timestamp, input.bpm, input.source ?? null, input.id]
@@ -2032,9 +2051,16 @@ export const nessaDbRouter = createTRPCRouter({
args: [input.id] args: [input.id]
}); });
if (sample.rows.length === 0) { 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({ await conn.execute({
sql: "DELETE FROM heartRateSamples WHERE id = ?", sql: "DELETE FROM heartRateSamples WHERE id = ?",
args: [input.id] args: [input.id]
@@ -2091,9 +2117,16 @@ export const nessaDbRouter = createTRPCRouter({
args: [input.id] args: [input.id]
}); });
if (sample.rows.length === 0) { 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({ await conn.execute({
sql: `UPDATE locationSamples SET timestamp = ?, latitude = ?, longitude = ?, altitude = ?, horizontalAccuracy = ?, verticalAccuracy = ?, speed = ?, course = ? WHERE id = ?`, sql: `UPDATE locationSamples SET timestamp = ?, latitude = ?, longitude = ?, altitude = ?, horizontalAccuracy = ?, verticalAccuracy = ?, speed = ?, course = ? WHERE id = ?`,
args: [ args: [
@@ -2128,9 +2161,16 @@ export const nessaDbRouter = createTRPCRouter({
args: [input.id] args: [input.id]
}); });
if (sample.rows.length === 0) { 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({ await conn.execute({
sql: "DELETE FROM locationSamples WHERE id = ?", sql: "DELETE FROM locationSamples WHERE id = ?",
args: [input.id] args: [input.id]
@@ -2188,9 +2228,16 @@ export const nessaDbRouter = createTRPCRouter({
args: [input.id] args: [input.id]
}); });
if (split.rows.length === 0) { 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({ await conn.execute({
sql: `UPDATE workoutSplits SET splitNumber = ?, distanceMeters = ?, durationSeconds = ?, startTimestamp = ?, endTimestamp = ?, averageHeartRate = ?, averagePace = ?, elevationGain = ?, elevationLoss = ? WHERE id = ?`, sql: `UPDATE workoutSplits SET splitNumber = ?, distanceMeters = ?, durationSeconds = ?, startTimestamp = ?, endTimestamp = ?, averageHeartRate = ?, averagePace = ?, elevationGain = ?, elevationLoss = ? WHERE id = ?`,
args: [ args: [
@@ -2226,9 +2273,16 @@ export const nessaDbRouter = createTRPCRouter({
args: [input.id] args: [input.id]
}); });
if (split.rows.length === 0) { 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({ await conn.execute({
sql: "DELETE FROM workoutSplits WHERE id = ?", sql: "DELETE FROM workoutSplits WHERE id = ?",
args: [input.id] args: [input.id]