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
|
# Keep directory structure
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
|
vanity-keypair-coop.json
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ model Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model SystemSettings {
|
model SystemSettings {
|
||||||
id String @id @default(cuid())
|
id String @id @default("default")
|
||||||
creditsPerMinute Int @default(2) @map("credits_per_minute")
|
creditsPerMinute Int @default(2) @map("credits_per_minute")
|
||||||
minWatchPercent Int @default(80) @map("min_watch_percent")
|
minWatchPercent Int @default(80) @map("min_watch_percent")
|
||||||
minWatchMinutes Int @default(5) @map("min_watch_minutes")
|
minWatchMinutes Int @default(5) @map("min_watch_minutes")
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { Request, Response, NextFunction } from 'express';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { prisma } from '../utils/prisma';
|
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 {
|
export interface AuthenticatedRequest extends Request {
|
||||||
user?: {
|
user?: {
|
||||||
|
|||||||
+44
-15
@@ -13,11 +13,17 @@ import { prisma } from "../utils/prisma";
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const OVERSEER_URL = process.env.OVERSEER_URL || "";
|
const OVERSEER_URL = process.env.OVERSEER_URL || "";
|
||||||
const OVERSEER_API_KEY = process.env.OVERSEER_API_KEY || "";
|
|
||||||
const overseerClient = axios.create({
|
let _overseerClient: ReturnType<typeof axios.create> | null = null;
|
||||||
baseURL: `${OVERSEER_URL}/api/v1`,
|
function getOverseerClient() {
|
||||||
headers: { "X-Api-Key": OVERSEER_API_KEY },
|
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 mapOverseerStatus = (status: any) => {
|
||||||
const s = Number(status);
|
const s = Number(status);
|
||||||
@@ -42,18 +48,41 @@ router.put(
|
|||||||
authenticate,
|
authenticate,
|
||||||
requireAdmin,
|
requireAdmin,
|
||||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
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({
|
const settings = await prisma.systemSettings.update({
|
||||||
where: { id: "default" },
|
where: { id: "default" },
|
||||||
data: {
|
data: {
|
||||||
creditsPerMinute: req.body.creditsPerMinute,
|
creditsPerMinute,
|
||||||
minWatchPercent: req.body.minWatchPercent,
|
minWatchPercent,
|
||||||
minWatchMinutes: req.body.minWatchMinutes,
|
minWatchMinutes,
|
||||||
movieRequestCost: req.body.movieRequestCost,
|
movieRequestCost,
|
||||||
tvRequestCost: req.body.tvRequestCost,
|
tvRequestCost,
|
||||||
tvPerSeasonCost: req.body.tvPerSeasonCost,
|
tvPerSeasonCost,
|
||||||
newReleaseMultiplier: req.body.newReleaseMultiplier,
|
newReleaseMultiplier,
|
||||||
bonusMultiplierActive: req.body.bonusMultiplierActive,
|
bonusMultiplierActive: !!req.body.bonusMultiplierActive,
|
||||||
bonusMultiplier: req.body.bonusMultiplier,
|
bonusMultiplier,
|
||||||
updatedBy: req.user!.id,
|
updatedBy: req.user!.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -293,7 +322,7 @@ router.post(
|
|||||||
const updated: any[] = [];
|
const updated: any[] = [];
|
||||||
for (const request of pending) {
|
for (const request of pending) {
|
||||||
try {
|
try {
|
||||||
const response = await overseerClient.get(
|
const response = await getOverseerClient().get(
|
||||||
`/request/${request.overseerRequestId}`,
|
`/request/${request.overseerRequestId}`,
|
||||||
);
|
);
|
||||||
const mapped = mapOverseerStatus(response.data?.status);
|
const mapped = mapOverseerStatus(response.data?.status);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ const router = Router();
|
|||||||
|
|
||||||
const PLEX_CLIENT_ID = process.env.PLEX_CLIENT_ID || "";
|
const PLEX_CLIENT_ID = process.env.PLEX_CLIENT_ID || "";
|
||||||
const PLEX_REDIRECT_URI = process.env.PLEX_REDIRECT_URI || "";
|
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(
|
router.get(
|
||||||
"/plex/url",
|
"/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(
|
const token = jwt.sign(
|
||||||
{ userId: user.id, plexId: user.plexId, isAdmin: user.isAdmin },
|
{ userId: user.id, plexId: user.plexId, isAdmin: user.isAdmin },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const API_URL =
|
|||||||
: "http://localhost:3001/api");
|
: "http://localhost:3001/api");
|
||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: `${API_URL}/api`,
|
baseURL: API_URL,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -56,12 +56,9 @@ export const userApi = {
|
|||||||
getLeaderboard: () => api.get("/users/leaderboard"),
|
getLeaderboard: () => api.get("/users/leaderboard"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wallet API
|
// Wallet API - deprecated, wallets removed
|
||||||
export const walletApi = {
|
export const walletApi = {
|
||||||
getWallet: () => api.get("/wallet"),
|
getBalance: () => api.get("/wallet"),
|
||||||
createWallet: () => api.post("/wallet/create"),
|
|
||||||
connectWallet: (address: string) => api.post("/wallet/connect", { address }),
|
|
||||||
backupWallet: () => api.post("/wallet/backup"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transactions API
|
// 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