chore: final solana purge

This commit is contained in:
2026-04-22 13:37:22 -04:00
parent 4bfcbb6b7e
commit f944b1f9b4
15 changed files with 374 additions and 1938 deletions
+7 -30
View File
@@ -1,12 +1,12 @@
import { Router } from "express";
import { io } from "../index";
import {
type AuthenticatedRequest,
authenticate,
AuthenticatedRequest,
requireAdmin,
} from "../middleware/auth";
import { asyncHandler } from "../middleware/errorHandler";
import { prisma } from "../utils/prisma";
import { asyncHandler } from "../middleware/errorHandler";
import { io } from "../index";
const router = Router();
@@ -20,7 +20,6 @@ router.get(
res.json(settings);
}),
);
router.put(
"/settings",
authenticate,
@@ -44,7 +43,6 @@ router.put(
res.json(settings);
}),
);
router.post(
"/pause",
authenticate,
@@ -54,10 +52,9 @@ router.post(
where: { id: "default" },
data: { mintingPaused: true, updatedBy: req.user!.id },
});
res.json({ message: "Minting paused" });
res.json({ message: "Paused" });
}),
);
router.post(
"/resume",
authenticate,
@@ -67,10 +64,9 @@ router.post(
where: { id: "default" },
data: { mintingPaused: false, updatedBy: req.user!.id },
});
res.json({ message: "Minting resumed" });
res.json({ message: "Resumed" });
}),
);
router.get(
"/users",
authenticate,
@@ -80,15 +76,12 @@ router.get(
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 100);
const skip = (pageNum - 1) * limitNum;
const where: any = {};
if (search) {
if (search)
where.OR = [
{ plexUsername: { contains: search as string, mode: "insensitive" } },
{ email: { contains: search as string, mode: "insensitive" } },
];
}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
@@ -102,7 +95,6 @@ router.get(
email: true,
isAdmin: true,
isActive: true,
walletAddress: true,
totalEarned: true,
totalSpent: true,
watchTimeMinutes: true,
@@ -111,7 +103,6 @@ router.get(
}),
prisma.user.count({ where }),
]);
res.json({
users,
pagination: {
@@ -123,7 +114,6 @@ router.get(
});
}),
);
router.put(
"/users/:id",
authenticate,
@@ -137,7 +127,6 @@ router.put(
res.json(user);
}),
);
router.post(
"/users/:id/bonus",
authenticate,
@@ -146,10 +135,8 @@ router.post(
const { amount, reason } = req.body;
if (!amount || amount <= 0)
return res.status(400).json({ error: "Valid amount required" });
const user = await prisma.user.findUnique({ where: { id: req.params.id } });
if (!user) return res.status(404).json({ error: "User not found" });
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
@@ -159,7 +146,6 @@ router.post(
contentTitle: reason || "Admin bonus",
},
});
await prisma.user.update({
where: { id: user.id },
data: { totalEarned: { increment: amount } },
@@ -172,7 +158,6 @@ router.post(
res.json({ success: true, amount, transaction });
}),
);
router.post(
"/users/:id/adjust",
authenticate,
@@ -181,10 +166,8 @@ router.post(
const { amount, reason } = req.body;
if (!amount || Number.isNaN(Number(amount)) || Number(amount) === 0)
return res.status(400).json({ error: "Valid amount required" });
const user = await prisma.user.findUnique({ where: { id: req.params.id } });
if (!user) return res.status(404).json({ error: "User not found" });
const delta = Number(amount);
await prisma.transaction.create({
data: {
@@ -195,7 +178,6 @@ router.post(
contentTitle: reason || "Admin adjustment",
},
});
await prisma.user.update({
where: { id: user.id },
data:
@@ -203,11 +185,9 @@ router.post(
? { totalEarned: { increment: delta } }
: { totalSpent: { increment: Math.abs(delta) } },
});
res.json({ success: true });
}),
);
router.get(
"/analytics",
authenticate,
@@ -217,15 +197,13 @@ router.get(
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const [userStats, transactionStats, watchStats, dailyActivity] =
await Promise.all([
prisma.$queryRaw`SELECT COUNT(*) as total, COUNT(CASE WHEN wallet_address IS NOT NULL THEN 1 END) as with_wallet, COUNT(CASE WHEN created_at >= ${sevenDaysAgo} THEN 1 END) as new_this_week FROM users`,
prisma.$queryRaw`SELECT COUNT(*) as total, COUNT(CASE WHEN created_at >= ${sevenDaysAgo} THEN 1 END) as new_this_week FROM users`,
prisma.$queryRaw`SELECT SUM(CASE WHEN type = 'EARN' THEN amount ELSE 0 END) as total_earned, SUM(CASE WHEN type = 'SPEND' THEN amount ELSE 0 END) as total_spent, COUNT(*) as total_transactions FROM transactions`,
prisma.$queryRaw`SELECT SUM(duration) as total_seconds, SUM(credits_earned) as total_credits, COUNT(*) as total_events FROM watch_events WHERE is_processed = true`,
prisma.$queryRaw`SELECT DATE(created_at) as date, COUNT(DISTINCT user_id) as active_users, SUM(CASE WHEN type = 'EARN' THEN amount ELSE 0 END) as earned, SUM(CASE WHEN type = 'SPEND' THEN amount ELSE 0 END) as spent FROM transactions WHERE created_at >= ${thirtyDaysAgo} GROUP BY DATE(created_at) ORDER BY date DESC LIMIT 30`,
]);
res.json({
users: userStats[0],
transactions: transactionStats[0],
@@ -234,5 +212,4 @@ router.get(
});
}),
);
export { router as adminRouter };
+22 -96
View File
@@ -6,24 +6,18 @@ 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,
},
headers: { "X-Api-Key": OVERSEER_API_KEY },
});
// Get request costs
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,
@@ -31,27 +25,20 @@ router.get(
});
}),
);
// Get user's balance and request availability
router.get(
"/balance",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user!.id },
});
if (!user?.walletAddress) {
const user = await prisma.user.findUnique({ where: { id: req.user!.id } });
if (!user)
return res.json({
hasWallet: false,
balance: 0,
canRequest: false,
canRequestMovie: false,
canRequestTV: false,
});
}
const settings = await prisma.systemSettings.findFirst();
const dbBalance = user.totalEarned - user.totalSpent;
res.json({
hasWallet: true,
balance: dbBalance,
@@ -64,76 +51,44 @@ router.get(
});
}),
);
// Search for content
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 },
});
if (!query) return res.status(400).json({ error: "Query required" });
const response = await overseerClient.get("/search", { params: { query } });
res.json(response.data);
}),
);
// Request content
router.post(
"/request",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { mediaType, mediaId, title, seasons } = req.body;
if (!mediaType || !mediaId || !title) {
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?.walletAddress) {
return res.status(400).json({ error: "Wallet required" });
}
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();
// Calculate cost
let cost = 0;
if (mediaType === "movie") {
cost = settings?.movieRequestCost || 500;
} else if (mediaType === "tv") {
cost = settings?.tvRequestCost || 1000;
if (seasons && seasons.length > 1) {
cost += (seasons.length - 1) * (settings?.tvPerSeasonCost || 250);
}
}
// Check balance
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) {
if (dbBalance < cost)
return res.status(400).json({
error: "Insufficient balance",
required: cost,
current: dbBalance,
});
}
// Create request in Overseer first
const overseerRequest = await overseerClient.post("/request", {
mediaType,
mediaId,
...(seasons && { seasons }),
});
// Create local request record
const request = await prisma.contentRequest.create({
data: {
userId: user.id,
@@ -146,8 +101,6 @@ router.post(
requestedAt: new Date(),
},
});
// Deduct credits immediately to prevent race condition
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
@@ -158,21 +111,15 @@ router.post(
contentTitle: title,
},
});
await prisma.user.update({
where: { id: user.id },
data: {
totalSpent: { increment: cost },
},
data: { totalSpent: { increment: cost } },
});
// Emit real-time spend update
io.to(`user:${user.id}`).emit("credits_spent", {
amount: cost,
title,
transaction,
});
res.json({
success: true,
request,
@@ -181,38 +128,23 @@ router.post(
});
}),
);
// Webhook: Handle Overseer request status changes
router.post(
"/webhook",
asyncHandler(async (req, res) => {
const { request_id, status } = req.body;
if (!request_id || !status) {
if (!request_id || !status)
return res.status(400).json({ error: "Missing fields" });
}
// Find local request
const request = await prisma.contentRequest.findFirst({
where: { overseerRequestId: request_id },
include: { user: true },
});
if (!request) {
return res.status(404).json({ error: "Request not found" });
}
// Update status
if (!request) return res.status(404).json({ error: "Request not found" });
const updatedRequest = await prisma.contentRequest.update({
where: { id: request.id },
data: { status },
});
// Credits were already deducted at request time.
// On approval: just confirm and link transaction to request.
// On decline: refund credits back to user.
if (status === "DECLINED") {
const refundTransaction = await prisma.transaction.create({
await prisma.transaction.create({
data: {
userId: request.userId,
type: "ADJUSTMENT",
@@ -221,21 +153,15 @@ router.post(
contentTitle: request.title,
},
});
await prisma.user.update({
where: { id: request.userId },
data: {
totalSpent: { decrement: request.creditsCost },
},
data: { totalSpent: { decrement: request.creditsCost } },
});
io.to(`user:${request.userId}`).emit("bonus_received", {
amount: request.creditsCost,
reason: `Refunded: ${request.title}`,
transaction: refundTransaction,
});
}
res.json({ success: true, request: updatedRequest });
}),
);
+141 -183
View File
@@ -1,193 +1,151 @@
import { Router } from 'express';
import { authenticate, AuthenticatedRequest, requireAdmin } from '../middleware/auth';
import { prisma } from '../utils/prisma';
import { asyncHandler } from '../middleware/errorHandler';
import { Router } from "express";
import {
type AuthenticatedRequest,
authenticate,
requireAdmin,
} from "../middleware/auth";
import { asyncHandler } from "../middleware/errorHandler";
import { prisma } from "../utils/prisma";
const router = Router();
// Get user's transactions
router.get('/',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = '1', limit = '20', type } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 100);
const skip = (pageNum - 1) * limitNum;
const where: any = { userId: req.user!.id };
if (type) {
where.type = type;
}
const [transactions, total] = await Promise.all([
prisma.transaction.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: limitNum,
include: {
watchEvent: {
select: {
duration: true,
percentComplete: true
}
},
request: {
select: {
mediaType: true,
status: true
}
}
}
}),
prisma.transaction.count({ where })
]);
res.json({
transactions,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
}
});
})
router.get(
"/",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = "1", limit = "20", type } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 100);
const skip = (pageNum - 1) * limitNum;
const where: any = { userId: req.user!.id };
if (type) where.type = type;
const [transactions, total] = await Promise.all([
prisma.transaction.findMany({
where,
orderBy: { createdAt: "desc" },
skip,
take: limitNum,
include: {
watchEvent: { select: { duration: true, percentComplete: true } },
request: { select: { mediaType: true, status: true } },
},
}),
prisma.transaction.count({ where }),
]);
res.json({
transactions,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum),
},
});
}),
);
// Get transaction stats
router.get('/stats',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const [
totalStats,
recentStats,
byType
] = await Promise.all([
// All time stats
prisma.transaction.groupBy({
by: ['type'],
where: { userId: req.user!.id },
_sum: { amount: true },
_count: { id: true }
}),
// Last 30 days
prisma.transaction.groupBy({
by: ['type'],
where: {
userId: req.user!.id,
createdAt: { gte: thirtyDaysAgo }
},
_sum: { amount: true },
_count: { id: true }
}),
// By type breakdown
prisma.transaction.findMany({
where: { userId: req.user!.id },
select: {
type: true,
amount: true,
createdAt: true
},
orderBy: { createdAt: 'desc' },
take: 100
})
]);
// Calculate daily earnings for chart
const dailyEarnings = await prisma.$queryRaw`
SELECT
DATE(created_at) as date,
SUM(CASE WHEN type = 'EARN' THEN amount ELSE 0 END) as earned,
SUM(CASE WHEN type = 'SPEND' THEN amount ELSE 0 END) as spent
FROM transactions
WHERE user_id = ${req.user!.id}
AND created_at >= ${thirtyDaysAgo}
GROUP BY DATE(created_at)
ORDER BY date DESC
`;
res.json({
total: totalStats,
recent: recentStats,
dailyEarnings,
recentTransactions: byType.slice(0, 10)
});
})
router.get(
"/stats",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const [totalStats, recentStats, byType] = await Promise.all([
prisma.transaction.groupBy({
by: ["type"],
where: { userId: req.user!.id },
_sum: { amount: true },
_count: { id: true },
}),
prisma.transaction.groupBy({
by: ["type"],
where: { userId: req.user!.id, createdAt: { gte: thirtyDaysAgo } },
_sum: { amount: true },
_count: { id: true },
}),
prisma.transaction.findMany({
where: { userId: req.user!.id },
select: { type: true, amount: true, createdAt: true },
orderBy: { createdAt: "desc" },
take: 100,
}),
]);
const dailyEarnings =
await prisma.$queryRaw`SELECT DATE(created_at) as date, SUM(CASE WHEN type = 'EARN' THEN amount ELSE 0 END) as earned, SUM(CASE WHEN type = 'SPEND' THEN amount ELSE 0 END) as spent FROM transactions WHERE user_id = ${req.user!.id} AND created_at >= ${thirtyDaysAgo} GROUP BY DATE(created_at) ORDER BY date DESC`;
res.json({
total: totalStats,
recent: recentStats,
dailyEarnings,
recentTransactions: byType.slice(0, 10),
});
}),
);
// Admin: Get all transactions
router.get('/admin/all',
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = '1', limit = '50', userId, type } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 100);
const skip = (pageNum - 1) * limitNum;
const where: any = {};
if (userId) where.userId = userId;
if (type) where.type = type;
const [transactions, total] = await Promise.all([
prisma.transaction.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: limitNum,
include: {
user: {
select: {
plexUsername: true,
walletAddress: true
}
}
}
}),
prisma.transaction.count({ where })
]);
res.json({
transactions,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
}
});
})
router.get(
"/admin/all",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = "1", limit = "50", userId, type } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 100);
const skip = (pageNum - 1) * limitNum;
const where: any = {};
if (userId) where.userId = userId;
if (type) where.type = type;
const [transactions, total] = await Promise.all([
prisma.transaction.findMany({
where,
orderBy: { createdAt: "desc" },
skip,
take: limitNum,
include: { user: { select: { plexUsername: true } } },
}),
prisma.transaction.count({ where }),
]);
res.json({
transactions,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum),
},
});
}),
);
// Get system-wide stats
router.get('/admin/stats',
// ... (keeping existing code)
router.get(
"/admin/stats",
authenticate,
requireAdmin,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const [total, recent] = await Promise.all([
prisma.transaction.groupBy({
by: ["type"],
_sum: { amount: true },
_count: { id: true },
}),
prisma.transaction.groupBy({
by: ["type"],
where: {
createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
},
_sum: { amount: true },
_count: { id: true },
}),
]);
res.json({ total, recent });
}),
);
// Get recent global activity
router.get('/recent',
authenticate,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const transactions = await prisma.transaction.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
include: {
user: {
select: {
plexUsername: true
}
}
}
});
res.json({ transactions });
})
router.get(
"/recent",
authenticate,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const transactions = await prisma.transaction.findMany({
orderBy: { createdAt: "desc" },
take: 10,
include: { user: { select: { plexUsername: true } } },
});
res.json({ transactions });
}),
);
export { router as transactionsRouter };
+130 -155
View File
@@ -1,165 +1,140 @@
import { Router } from 'express';
import { authenticate, AuthenticatedRequest } from '../middleware/auth';
import { prisma } from '../utils/prisma';
import { asyncHandler } from '../middleware/errorHandler';
import { Router } from "express";
import { type AuthenticatedRequest, authenticate } from "../middleware/auth";
import { asyncHandler } from "../middleware/errorHandler";
import { prisma } from "../utils/prisma";
const router = Router();
// Get current user profile
router.get('/me',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user!.id },
select: {
id: true,
plexId: true,
plexUsername: true,
email: true,
isAdmin: true,
walletAddress: true,
totalEarned: true,
totalSpent: true,
watchTimeMinutes: true,
createdAt: true
}
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
})
router.get(
"/me",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user!.id },
select: {
id: true,
plexId: true,
plexUsername: true,
email: true,
isAdmin: true,
totalEarned: true,
totalSpent: true,
watchTimeMinutes: true,
createdAt: true,
},
});
if (!user) return res.status(404).json({ error: "User not found" });
res.json(user);
}),
);
// Update user profile
router.put('/me',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { email } = req.body;
const user = await prisma.user.update({
where: { id: req.user!.id },
data: { email },
select: {
id: true,
plexUsername: true,
email: true,
walletAddress: true
}
});
res.json(user);
})
router.put(
"/me",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { email } = req.body;
const user = await prisma.user.update({
where: { id: req.user!.id },
data: { email },
select: { id: true, plexUsername: true, email: true },
});
res.json(user);
}),
);
// Get user's watch history
router.get('/me/watch-history',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = '1', limit = '20' } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 50);
const skip = (pageNum - 1) * limitNum;
const [events, total] = await Promise.all([
prisma.watchEvent.findMany({
where: { userId: req.user!.id },
orderBy: { watchedAt: 'desc' },
skip,
take: limitNum,
select: {
id: true,
contentType: true,
title: true,
grandparentTitle: true,
duration: true,
percentComplete: true,
creditsEarned: true,
isProcessed: true,
watchedAt: true
}
}),
prisma.watchEvent.count({ where: { userId: req.user!.id } })
]);
res.json({
events,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
}
});
})
router.get(
"/me/watch-history",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = "1", limit = "20" } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 50);
const skip = (pageNum - 1) * limitNum;
const [events, total] = await Promise.all([
prisma.watchEvent.findMany({
where: { userId: req.user!.id },
orderBy: { watchedAt: "desc" },
skip,
take: limitNum,
select: {
id: true,
contentType: true,
title: true,
grandparentTitle: true,
duration: true,
percentComplete: true,
creditsEarned: true,
isProcessed: true,
watchedAt: true,
},
}),
prisma.watchEvent.count({ where: { userId: req.user!.id } }),
]);
res.json({
events,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum),
},
});
}),
);
// Get user's content requests
router.get('/me/requests',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = '1', limit = '20', status } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 50);
const skip = (pageNum - 1) * limitNum;
const where: any = { userId: req.user!.id };
if (status) where.status = status;
const [requests, total] = await Promise.all([
prisma.contentRequest.findMany({
where,
orderBy: { requestedAt: 'desc' },
skip,
take: limitNum
}),
prisma.contentRequest.count({ where })
]);
res.json({
requests,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
}
});
})
router.get(
"/me/requests",
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = "1", limit = "20", status } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 50);
const skip = (pageNum - 1) * limitNum;
const where: any = { userId: req.user!.id };
if (status) where.status = status;
const [requests, total] = await Promise.all([
prisma.contentRequest.findMany({
where,
orderBy: { requestedAt: "desc" },
skip,
take: limitNum,
}),
prisma.contentRequest.count({ where }),
]);
res.json({
requests,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum),
},
});
}),
);
// Get leaderboard
router.get('/leaderboard',
authenticate,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const topEarners = await prisma.user.findMany({
orderBy: { totalEarned: 'desc' },
take: 10,
select: {
id: true,
plexUsername: true,
totalEarned: true,
watchTimeMinutes: true
}
});
const topWatchers = await prisma.user.findMany({
orderBy: { watchTimeMinutes: 'desc' },
take: 10,
select: {
id: true,
plexUsername: true,
totalEarned: true,
watchTimeMinutes: true
}
});
res.json({ topEarners, topWatchers });
})
router.get(
"/leaderboard",
authenticate,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const topEarners = await prisma.user.findMany({
orderBy: { totalEarned: "desc" },
take: 10,
select: {
id: true,
plexUsername: true,
totalEarned: true,
watchTimeMinutes: true,
},
});
const topWatchers = await prisma.user.findMany({
orderBy: { watchTimeMinutes: "desc" },
take: 10,
select: {
id: true,
plexUsername: true,
totalEarned: true,
watchTimeMinutes: true,
},
});
res.json({ topEarners, topWatchers });
}),
);
export { router as userRouter };
+3 -27
View File
@@ -21,55 +21,37 @@ router.post(
asyncHandler(async (req, res) => {
const webhookSignature = req.headers["x-tautulli-signature"] as string;
const payload = JSON.stringify(req.body);
if (
WEBHOOK_SECRET &&
webhookSignature &&
!verifyWebhookSignature(payload, webhookSignature)
) {
)
return res.status(401).json({ error: "Invalid signature" });
}
const event = req.body;
if (event.action !== "watched")
return res.json({ message: "Event type not processed" });
if (!event.user_id || !event.rating_key || !event.session_key)
return res.status(400).json({ error: "Missing required fields" });
const user = await prisma.user.findUnique({
where: { plexId: event.user_id.toString() },
});
if (!user) return res.status(404).json({ error: "User not found" });
if (!user.walletAddress)
return res.status(400).json({ error: "User has no wallet" });
const existing = await prisma.watchEvent.findUnique({
where: { sessionId: event.session_key.toString() },
});
if (existing) return res.json({ message: "Event already processed" });
const settings = await prisma.systemSettings.findFirst();
const creditsPerMinute = settings?.creditsPerMinute || 2;
const minWatchPercent = settings?.minWatchPercent || 80;
const minWatchMinutes = settings?.minWatchMinutes || 5;
const watchDurationMinutes = Math.floor(
(event.stopped - event.started) / 60,
);
const percentComplete = event.percent_complete || 0;
if (percentComplete < minWatchPercent)
return res.json({
message: "Watch percentage too low",
percentComplete,
required: minWatchPercent,
});
return res.json({ message: "Watch percentage too low" });
if (watchDurationMinutes < minWatchMinutes)
return res.json({
message: "Watch duration too short",
watchDurationMinutes,
required: minWatchMinutes,
});
return res.json({ message: "Watch duration too short" });
let creditsEarned = watchDurationMinutes * creditsPerMinute;
if (settings?.newReleaseMultiplier && event.is_new)
creditsEarned = Math.floor(
@@ -79,7 +61,6 @@ router.post(
creditsEarned = Math.floor(
creditsEarned * Number(settings.bonusMultiplier),
);
const watchEvent = await prisma.watchEvent.create({
data: {
userId: user.id,
@@ -94,7 +75,6 @@ router.post(
watchedAt: new Date(event.stopped * 1000),
},
});
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
@@ -105,7 +85,6 @@ router.post(
contentTitle: event.title,
},
});
await prisma.user.update({
where: { id: user.id },
data: {
@@ -113,12 +92,10 @@ router.post(
watchTimeMinutes: { increment: watchDurationMinutes },
},
});
await prisma.watchEvent.update({
where: { id: watchEvent.id },
data: { isProcessed: true },
});
io.to(`user:${user.id}`).emit("credits_earned", {
amount: creditsEarned,
title: event.title,
@@ -130,7 +107,6 @@ router.post(
createdAt: transaction.createdAt,
},
});
res.json({
success: true,
creditsEarned,