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:
@@ -72,3 +72,4 @@ tmp/
|
||||
|
||||
# Keep directory structure
|
||||
!.gitkeep
|
||||
vanity-keypair-coop.json
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
Reference in New Issue
Block a user