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)
This commit is contained in:
2026-04-30 14:41:48 -04:00
parent 46cbb4e551
commit d5adc2f985
7 changed files with 63 additions and 29 deletions
+1
View File
@@ -72,3 +72,4 @@ tmp/
# Keep directory structure
!.gitkeep
vanity-keypair-coop.json
+1 -1
View File
@@ -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")
+4 -1
View File
@@ -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?: {
+44 -15
View File
@@ -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<typeof axios.create> | 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);
+10 -2
View File
@@ -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 },
+3 -6
View File
@@ -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
-4
View File
@@ -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]
}