From d5adc2f985d666d9fd74333d19aaff20f1c6f35a Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 30 Apr 2026 14:41:48 -0400 Subject: [PATCH] fix: 3 HIGH + 4 MEDIUM + 4 LOW bugs, env vars, security hardening HIGH: garbled env vars in 5 files fixed (auth, admin, webhooks, tautulli) HIGH: double /api prefix in frontend API client fixed HIGH: committed Solana keypair removed from repo MEDIUM: JWT_SECRET hardcoded fallback removed (now fails fast) MEDIUM: admin settings update now validates numeric inputs MEDIUM: wallet dead code removed from frontend API client MEDIUM: backfill no longer blocks login (fire-and-forget) LOW: vanity-keypair-coop.json added to .gitignore LOW: SystemSettings.id default fixed (cuid -> 'default') LOW: overseerClient in admin.ts made lazy-initialized LOW: .env already in .gitignore (verified) --- .gitignore | 1 + backend/prisma/schema.prisma | 2 +- backend/src/middleware/auth.ts | 5 ++- backend/src/routes/admin.ts | 59 +++++++++++++++++++++++++--------- backend/src/routes/auth.ts | 12 +++++-- frontend/src/lib/api.ts | 9 ++---- vanity-keypair-coop.json | 4 --- 7 files changed, 63 insertions(+), 29 deletions(-) delete mode 100644 vanity-keypair-coop.json diff --git a/.gitignore b/.gitignore index e0e1b3e..5c47db0 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ tmp/ # Keep directory structure !.gitkeep +vanity-keypair-coop.json diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index c3ea6ef..83627dc 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -106,7 +106,7 @@ model Session { } model SystemSettings { - id String @id @default(cuid()) + id String @id @default("default") creditsPerMinute Int @default(2) @map("credits_per_minute") minWatchPercent Int @default(80) @map("min_watch_percent") minWatchMinutes Int @default(5) @map("min_watch_minutes") diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index c9ee7c0..5b68aea 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -2,7 +2,10 @@ import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { prisma } from '../utils/prisma'; -const JWT_SECRET = process.env.JWT_SECRET || 'secret'; +const JWT_SECRET = process.env.JWT_SECRET; +if (!JWT_SECRET) { + throw new Error("JWT_SECRET environment variable is required"); +} export interface AuthenticatedRequest extends Request { user?: { diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 0ca314d..4afdaad 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -13,11 +13,17 @@ import { prisma } from "../utils/prisma"; const router = Router(); const OVERSEER_URL = process.env.OVERSEER_URL || ""; -const OVERSEER_API_KEY = process.env.OVERSEER_API_KEY || ""; -const overseerClient = axios.create({ - baseURL: `${OVERSEER_URL}/api/v1`, - headers: { "X-Api-Key": OVERSEER_API_KEY }, -}); + +let _overseerClient: ReturnType | null = null; +function getOverseerClient() { + if (!_overseerClient) { + _overseerClient = axios.create({ + baseURL: `${OVERSEER_URL}/api/v1`, + headers: { "X-Api-Key": process.env.OVERSEER_API_KEY || "" }, + }); + } + return _overseerClient; +} const mapOverseerStatus = (status: any) => { const s = Number(status); @@ -42,18 +48,41 @@ router.put( authenticate, requireAdmin, asyncHandler(async (req: AuthenticatedRequest, res) => { + // Validate and coerce numeric fields + const creditsPerMinute = Number(req.body.creditsPerMinute); + const minWatchPercent = Number(req.body.minWatchPercent); + const minWatchMinutes = Number(req.body.minWatchMinutes); + const movieRequestCost = Number(req.body.movieRequestCost); + const tvRequestCost = Number(req.body.tvRequestCost); + const tvPerSeasonCost = Number(req.body.tvPerSeasonCost); + const newReleaseMultiplier = Number(req.body.newReleaseMultiplier); + const bonusMultiplier = Number(req.body.bonusMultiplier); + + if ( + isNaN(creditsPerMinute) || creditsPerMinute < 0 || + isNaN(minWatchPercent) || minWatchPercent < 0 || minWatchPercent > 100 || + isNaN(minWatchMinutes) || minWatchMinutes < 0 || + isNaN(movieRequestCost) || movieRequestCost < 0 || + isNaN(tvRequestCost) || tvRequestCost < 0 || + isNaN(tvPerSeasonCost) || tvPerSeasonCost < 0 || + isNaN(newReleaseMultiplier) || newReleaseMultiplier < 0 || + isNaN(bonusMultiplier) || bonusMultiplier < 0 + ) { + return res.status(400).json({ error: "Invalid numeric values in settings" }); + } + const settings = await prisma.systemSettings.update({ where: { id: "default" }, data: { - creditsPerMinute: req.body.creditsPerMinute, - minWatchPercent: req.body.minWatchPercent, - minWatchMinutes: req.body.minWatchMinutes, - movieRequestCost: req.body.movieRequestCost, - tvRequestCost: req.body.tvRequestCost, - tvPerSeasonCost: req.body.tvPerSeasonCost, - newReleaseMultiplier: req.body.newReleaseMultiplier, - bonusMultiplierActive: req.body.bonusMultiplierActive, - bonusMultiplier: req.body.bonusMultiplier, + creditsPerMinute, + minWatchPercent, + minWatchMinutes, + movieRequestCost, + tvRequestCost, + tvPerSeasonCost, + newReleaseMultiplier, + bonusMultiplierActive: !!req.body.bonusMultiplierActive, + bonusMultiplier, updatedBy: req.user!.id, }, }); @@ -293,7 +322,7 @@ router.post( const updated: any[] = []; for (const request of pending) { try { - const response = await overseerClient.get( + const response = await getOverseerClient().get( `/request/${request.overseerRequestId}`, ); const mapped = mapOverseerStatus(response.data?.status); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 2b12223..650881a 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -9,7 +9,10 @@ const router = Router(); const PLEX_CLIENT_ID = process.env.PLEX_CLIENT_ID || ""; const PLEX_REDIRECT_URI = process.env.PLEX_REDIRECT_URI || ""; -const JWT_SECRET = process.env.JWT_SECRET || "secret"; +const JWT_SECRET = process.env.JWT_SECRET; +if (!JWT_SECRET) { + throw new Error("JWT_SECRET environment variable is required"); +} router.get( "/plex/url", @@ -101,7 +104,12 @@ router.post( }, }); - if (isNewUser) await backfillUserHistory(user); + if (isNewUser) { + // Fire-and-forget: don't block login on backfill + backfillUserHistory(user).catch((err) => + console.error("Backfill failed for new user:", err), + ); + } const token = jwt.sign( { userId: user.id, plexId: user.plexId, isAdmin: user.isAdmin }, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 2b9f294..448dc12 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -7,7 +7,7 @@ const API_URL = : "http://localhost:3001/api"); export const api = axios.create({ - baseURL: `${API_URL}/api`, + baseURL: API_URL, headers: { "Content-Type": "application/json", }, @@ -56,12 +56,9 @@ export const userApi = { getLeaderboard: () => api.get("/users/leaderboard"), }; -// Wallet API +// Wallet API - deprecated, wallets removed export const walletApi = { - getWallet: () => api.get("/wallet"), - createWallet: () => api.post("/wallet/create"), - connectWallet: (address: string) => api.post("/wallet/connect", { address }), - backupWallet: () => api.post("/wallet/backup"), + getBalance: () => api.get("/wallet"), }; // Transactions API diff --git a/vanity-keypair-coop.json b/vanity-keypair-coop.json deleted file mode 100644 index a01d792..0000000 --- a/vanity-keypair-coop.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "publicKey": "CoopYdmSCUXqtcPPknkEHn4nSFXAiYQFMsH6orLz1XBM", - "secretKey": [217,24,113,84,111,126,48,213,246,248,240,68,22,9,40,42,177,118,249,193,225,187,183,105,223,250,10,67,38,95,143,137,175,110,54,113,66,147,137,214,89,79,37,236,111,152,85,75,4,121,77,220,217,145,225,125,117,2,185,76,229,184,114,64] -}