Files
coop/backend/src/routes/overseer.ts
T

186 lines
5.3 KiB
TypeScript

import axios from "axios";
import { Router } from "express";
import { io } from "../index";
import { type AuthenticatedRequest, authenticate } from "../middleware/auth";
import { asyncHandler } from "../middleware/errorHandler";
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 },
});
router.get(
"/costs",
authenticate,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const settings = await prisma.systemSettings.findFirst();
res.json({
movie: settings?.movieRequestCost || 500,
tv: settings?.tvRequestCost || 1000,
tvPerSeason: settings?.tvPerSeasonCost || 250,
});
}),
);
router.get(
"/balance",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const user = await prisma.user.findUnique({ where: { id: req.user!.id } });
if (!user)
return res.json({
hasWallet: false,
balance: 0,
canRequestMovie: false,
canRequestTV: false,
});
const settings = await prisma.systemSettings.findFirst();
const dbBalance = user.totalEarned - user.totalSpent;
res.json({
hasWallet: true,
balance: dbBalance,
canRequestMovie: dbBalance >= (settings?.movieRequestCost || 500),
canRequestTV: dbBalance >= (settings?.tvRequestCost || 1000),
costs: {
movie: settings?.movieRequestCost || 500,
tv: settings?.tvRequestCost || 1000,
},
});
}),
);
router.get(
"/search",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { query } = req.query;
if (!query) return res.status(400).json({ error: "Query required" });
const response = await overseerClient.get("/search", { params: { query } });
res.json(response.data);
}),
);
router.post(
"/request",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { mediaType, mediaId, title, seasons } = req.body;
if (!mediaType || !mediaId || !title)
return res.status(400).json({ error: "Missing required fields" });
const user = await prisma.user.findUnique({ where: { id: req.user!.id } });
if (!user) return res.status(400).json({ error: "User required" });
const settings = await prisma.systemSettings.findFirst();
let cost =
mediaType === "movie"
? settings?.movieRequestCost || 500
: settings?.tvRequestCost || 1000;
if (mediaType === "tv" && seasons && seasons.length > 1)
cost += (seasons.length - 1) * (settings?.tvPerSeasonCost || 250);
const dbBalance = user.totalEarned - user.totalSpent;
if (dbBalance < cost)
return res
.status(400)
.json({
error: "Insufficient balance",
required: cost,
current: dbBalance,
});
const overseerRequest = await overseerClient.post("/request", {
mediaType,
mediaId,
...(seasons && { seasons }),
});
const request = await prisma.contentRequest.create({
data: {
userId: user.id,
overseerRequestId: overseerRequest.data.id,
mediaType,
tmdbId: mediaId,
title,
creditsCost: cost,
status: "PENDING",
requestedAt: new Date(),
},
});
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
type: "SPEND",
amount: cost,
requestId: request.id,
description: `Request: ${title}`,
contentTitle: title,
},
});
await prisma.user.update({
where: { id: user.id },
data: { totalSpent: { increment: cost } },
});
io.to(`user:${user.id}`).emit("credits_spent", {
amount: cost,
title,
transaction,
});
res.json({
success: true,
request,
cost,
message: `Request submitted. ${cost} $COOP deducted.`,
});
}),
);
router.post(
"/webhook",
asyncHandler(async (req, res) => {
const rawId =
req.body.request_id ??
req.body.requestId ??
req.body.id ??
req.body.overseer_request_id;
const statusRaw = String(req.body.status || "").toUpperCase();
let status = "PENDING";
if (statusRaw === "APPROVED" || statusRaw === "COMPLETED")
status = "APPROVED";
else if (statusRaw === "DECLINED" || statusRaw === "FAILED")
status = "DECLINED";
if (!rawId) return res.status(400).json({ error: "Missing fields" });
const request = await prisma.contentRequest.findFirst({
where: { overseerRequestId: Number(rawId) },
include: { user: true },
});
if (!request) return res.status(404).json({ error: "Request not found" });
const updatedRequest = await prisma.contentRequest.update({
where: { id: request.id },
data: { status: status as any },
});
if (status === "DECLINED") {
await prisma.transaction.create({
data: {
userId: request.userId,
type: "ADJUSTMENT",
amount: request.creditsCost,
description: `Refunded: ${request.title}`,
contentTitle: request.title,
},
});
await prisma.user.update({
where: { id: request.userId },
data: { totalSpent: { decrement: request.creditsCost } },
});
io.to(`user:${request.userId}`).emit("bonus_received", {
amount: request.creditsCost,
reason: `Refunded: ${request.title}`,
});
}
io.to(`user:${request.userId}`).emit("request_updated", {
id: request.id,
status: updatedRequest.status,
title: request.title,
});
res.json({ success: true, request: updatedRequest });
}),
);
export { router as overseerRouter };