chore: final solana purge
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user