refactor: db-only credits and admin controls

Remove blockchain dependence from credit flow:
- watch credits now DB-only
- webhook earnings now DB-only
- wallet balance now DB-only
- admin bonus/adjustment routes operate on DB totals

Add admin adjustment API for account control.
Keep Solana service file unused for now; safe to delete later.
This commit is contained in:
2026-04-22 13:05:48 -04:00
parent 86a8c7d89a
commit 8ec91acc25
5 changed files with 327 additions and 470 deletions
+209 -287
View File
@@ -1,316 +1,238 @@
import { Router } from 'express';
import { authenticate, AuthenticatedRequest, requireAdmin } from '../middleware/auth';
import { prisma } from '../utils/prisma';
import { asyncHandler } from '../middleware/errorHandler';
import { mintTokens } from '../services/solana';
import { io } from '../index';
import { Router } from "express";
import { io } from "../index";
import {
type AuthenticatedRequest,
authenticate,
requireAdmin,
} from "../middleware/auth";
import { asyncHandler } from "../middleware/errorHandler";
import { prisma } from "../utils/prisma";
const router = Router();
// Get system settings
router.get('/settings',
authenticate,
requireAdmin,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const settings = await prisma.systemSettings.findFirst();
if (!settings) {
return res.status(404).json({ error: 'Settings not found' });
}
res.json(settings);
})
router.get(
"/settings",
authenticate,
requireAdmin,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const settings = await prisma.systemSettings.findFirst();
if (!settings) return res.status(404).json({ error: "Settings not found" });
res.json(settings);
}),
);
// Update system settings
router.put('/settings',
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const {
creditsPerMinute,
minWatchPercent,
minWatchMinutes,
movieRequestCost,
tvRequestCost,
newReleaseMultiplier,
bonusMultiplierActive,
bonusMultiplier
} = req.body;
const settings = await prisma.systemSettings.update({
where: { id: 'default' },
data: {
creditsPerMinute,
minWatchPercent,
minWatchMinutes,
movieRequestCost,
tvRequestCost,
newReleaseMultiplier,
bonusMultiplierActive,
bonusMultiplier,
updatedBy: req.user!.id
}
});
res.json(settings);
})
router.put(
"/settings",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
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,
updatedBy: req.user!.id,
},
});
res.json(settings);
}),
);
// Pause minting
router.post('/pause',
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
await prisma.systemSettings.update({
where: { id: 'default' },
data: {
mintingPaused: true,
updatedBy: req.user!.id
}
});
res.json({ message: 'Minting paused' });
})
router.post(
"/pause",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
await prisma.systemSettings.update({
where: { id: "default" },
data: { mintingPaused: true, updatedBy: req.user!.id },
});
res.json({ message: "Minting paused" });
}),
);
// Resume minting
router.post('/resume',
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
await prisma.systemSettings.update({
where: { id: 'default' },
data: {
mintingPaused: false,
updatedBy: req.user!.id
}
});
res.json({ message: 'Minting resumed' });
})
router.post(
"/resume",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
await prisma.systemSettings.update({
where: { id: "default" },
data: { mintingPaused: false, updatedBy: req.user!.id },
});
res.json({ message: "Minting resumed" });
}),
);
// Get all users
router.get('/users',
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = '1', limit = '50', search } = req.query;
const pageNum = parseInt(page as string);
const limitNum = Math.min(parseInt(limit as string), 100);
const skip = (pageNum - 1) * limitNum;
router.get(
"/users",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { page = "1", limit = "50", search } = 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 (search) {
where.OR = [
{ plexUsername: { contains: search as string, mode: 'insensitive' } },
{ email: { contains: search as string, mode: 'insensitive' } }
];
}
const where: any = {};
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,
orderBy: { createdAt: 'desc' },
skip,
take: limitNum,
select: {
id: true,
plexId: true,
plexUsername: true,
email: true,
isAdmin: true,
isActive: true,
walletAddress: true,
totalEarned: true,
totalSpent: true,
watchTimeMinutes: true,
createdAt: true
}
}),
prisma.user.count({ where })
]);
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
orderBy: { createdAt: "desc" },
skip,
take: limitNum,
select: {
id: true,
plexId: true,
plexUsername: true,
email: true,
isAdmin: true,
isActive: true,
walletAddress: true,
totalEarned: true,
totalSpent: true,
watchTimeMinutes: true,
createdAt: true,
},
}),
prisma.user.count({ where }),
]);
res.json({
users,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
}
});
})
res.json({
users,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum),
},
});
}),
);
// Update user
router.put('/users/:id',
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { isAdmin, isActive } = req.body;
const user = await prisma.user.update({
where: { id: req.params.id },
data: {
isAdmin,
isActive
},
select: {
id: true,
plexUsername: true,
isAdmin: true,
isActive: true
}
});
res.json(user);
})
router.put(
"/users/:id",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const user = await prisma.user.update({
where: { id: req.params.id },
data: { isAdmin: req.body.isAdmin, isActive: req.body.isActive },
select: { id: true, plexUsername: true, isAdmin: true, isActive: true },
});
res.json(user);
}),
);
// Grant bonus credits
router.post('/users/:id/bonus',
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { amount, reason } = req.body;
router.post(
"/users/:id/bonus",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { amount, reason } = req.body;
if (!amount || amount <= 0)
return res.status(400).json({ error: "Valid amount required" });
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 user = await prisma.user.findUnique({
where: { id: req.params.id }
});
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
type: "BONUS",
amount,
description: reason || "Admin bonus",
contentTitle: reason || "Admin bonus",
},
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
if (!user.walletAddress) {
return res.status(400).json({ error: 'User has no wallet' });
}
// Mint bonus tokens
const signature = await mintTokens(
user.walletAddress,
amount,
{
sessionId: `BONUS-${Date.now()}`,
contentTitle: reason || 'Admin Bonus',
watchDurationMinutes: 0
}
);
if (!signature) {
return res.status(500).json({ error: 'Failed to mint bonus' });
}
// Create transaction record
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
type: 'BONUS',
amount,
solanaSignature: signature,
description: reason || 'Admin bonus',
contentTitle: reason || 'Admin Bonus'
}
});
// Update user stats
await prisma.user.update({
where: { id: user.id },
data: {
totalEarned: { increment: amount }
}
});
// Emit real-time update via WebSocket
io.to(`user:${user.id}`).emit('bonus_received', {
amount,
reason: reason || 'Admin Bonus',
transaction
});
res.json({
success: true,
amount,
solanaSignature: signature,
transaction
});
})
await prisma.user.update({
where: { id: user.id },
data: { totalEarned: { increment: amount } },
});
io.to(`user:${user.id}`).emit("bonus_received", {
amount,
reason: reason || "Admin bonus",
transaction,
});
res.json({ success: true, amount, transaction });
}),
);
// Get dashboard analytics
router.get('/analytics',
authenticate,
requireAdmin,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
router.post(
"/users/:id/adjust",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { amount, reason } = req.body;
if (!amount || Number.isNaN(Number(amount)) || Number(amount) === 0)
return res.status(400).json({ error: "Valid amount required" });
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const user = await prisma.user.findUnique({ where: { id: req.params.id } });
if (!user) return res.status(404).json({ error: "User not found" });
const [
userStats,
transactionStats,
watchStats,
dailyActivity
] = await Promise.all([
// User stats
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
`,
// Transaction stats
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
`,
// Watch stats
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
`,
// Daily activity (last 30 days)
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
`
]);
const delta = Number(amount);
await prisma.transaction.create({
data: {
userId: user.id,
type: "ADJUSTMENT",
amount: Math.abs(delta),
description: reason || "Admin adjustment",
contentTitle: reason || "Admin adjustment",
},
});
res.json({
users: userStats[0],
transactions: transactionStats[0],
watchStats: watchStats[0],
dailyActivity
});
})
await prisma.user.update({
where: { id: user.id },
data:
delta > 0
? { totalEarned: { increment: delta } }
: { totalSpent: { increment: Math.abs(delta) } },
});
res.json({ success: true });
}),
);
router.get(
"/analytics",
authenticate,
requireAdmin,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const sevenDaysAgo = new Date();
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 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],
watchStats: watchStats[0],
dailyActivity,
});
}),
);
export { router as adminRouter };
+14 -24
View File
@@ -3,7 +3,7 @@ import crypto from "crypto";
import { Router } from "express";
import jwt from "jsonwebtoken";
import { asyncHandler } from "../middleware/errorHandler";
import { createWallet, mintTokens } from "../services/solana";
import { createWallet } from "../services/solana";
import { prisma } from "../utils/prisma";
const ENCRYPTION_KEY =
@@ -374,31 +374,21 @@ async function backfillUserHistory(user: {
},
});
// Mint tokens on Solana
const signature = await mintTokens(user.walletAddress!, creditsEarned, {
sessionId,
contentTitle: item.title || item.full_title || "Unknown",
watchDurationMinutes,
// DB-only credit record
await prisma.transaction.create({
data: {
userId: user.id,
type: "EARN",
amount: creditsEarned,
watchEventId: watchEvent.id,
description: `Watched ${item.title || item.full_title || "Unknown"}`,
contentTitle: item.title || item.full_title || "Unknown",
},
});
if (signature) {
// Create transaction record
await prisma.transaction.create({
data: {
userId: user.id,
type: "EARN",
amount: creditsEarned,
watchEventId: watchEvent.id,
solanaSignature: signature,
description: `Watched ${item.title || item.full_title || "Unknown"}`,
contentTitle: item.title || item.full_title || "Unknown",
},
});
totalCredits += creditsEarned;
totalMinutes += watchDurationMinutes;
processedCount++;
}
totalCredits += creditsEarned;
totalMinutes += watchDurationMinutes;
processedCount++;
}
// Update user stats
+59 -49
View File
@@ -6,18 +6,13 @@ import {
requireAdmin,
} from "../middleware/auth";
import { asyncHandler } from "../middleware/errorHandler";
import {
createWallet,
getTokenBalance,
requestAirdrop,
} from "../services/solana";
import { createWallet } from "../services/solana";
import { prisma } from "../utils/prisma";
const router = Router();
const ENCRYPTION_KEY =
process.env.ENCRYPTION_KEY || "default-key-32-chars-long!!!!!";
// Encrypt private key
function encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest();
@@ -28,7 +23,6 @@ function encrypt(text: string): string {
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
}
// Decrypt private key
function decrypt(encryptedData: string): string {
const parts = encryptedData.split(":");
const iv = Buffer.from(parts[0], "hex");
@@ -42,7 +36,6 @@ function decrypt(encryptedData: string): string {
return decrypted;
}
// Get user's wallet info
router.get(
"/",
authenticate,
@@ -65,24 +58,18 @@ router.get(
});
}
// Get on-chain balance (may be 0 if using DB-only credits)
const onChainBalance = await getTokenBalance(user.walletAddress);
// Usable credits are tracked in DB (works even when blockchain is down)
const dbBalance = user.totalEarned - user.totalSpent;
res.json({
hasWallet: true,
address: user.walletAddress,
balance: dbBalance,
onChainBalance,
totalEarned: user.totalEarned,
totalSpent: user.totalSpent,
explorerUrl: `https://explorer.solana.com/address/${user.walletAddress}?cluster=devnet`,
});
}),
);
// Create new wallet
router.post(
"/create",
authenticate,
@@ -95,7 +82,6 @@ router.post(
return res.status(400).json({ error: "Wallet already exists" });
}
// Create new Solana wallet
const wallet = createWallet();
const encryptedKey = encrypt(wallet.secretKey);
@@ -107,20 +93,13 @@ router.post(
},
});
// Request airdrop for testing
await requestAirdrop(wallet.publicKey);
res.json({
success: true,
address: wallet.publicKey,
message:
"Wallet created successfully. Funded with 2 SOL for transaction fees.",
warning:
"Please backup your recovery phrase if shown. This is the only time it will be displayed.",
});
}),
);
// Connect existing wallet
router.post(
"/connect",
authenticate,
@@ -128,21 +107,15 @@ router.post(
const { address } = req.body;
if (!address) {
return res.status(400).json({ error: "Wallet address required" });
return res.status(400).json({ error: "Address required" });
}
// Check if address is already connected to another user
const existing = await prisma.user.findFirst({
where: {
walletAddress: address,
NOT: { id: req.user!.id },
},
where: { walletAddress: address },
});
if (existing) {
return res
.status(400)
.json({ error: "Wallet already connected to another account" });
if (existing && existing.id !== req.user!.id) {
return res.status(400).json({ error: "Wallet already in use" });
}
await prisma.user.update({
@@ -150,14 +123,10 @@ router.post(
data: { walletAddress: address },
});
res.json({
address,
message: "Wallet connected successfully",
});
res.json({ success: true, address });
}),
);
// Get recovery phrase (only shown once at creation)
router.post(
"/backup",
authenticate,
@@ -167,20 +136,14 @@ router.post(
});
if (!user?.encryptedPrivateKey) {
return res.status(400).json({ error: "No wallet found" });
return res.status(400).json({ error: "No wallet backup available" });
}
// Decrypt and return private key for backup
const privateKey = decrypt(user.encryptedPrivateKey);
res.json({
privateKey,
warning: "Store this securely. Never share it with anyone.",
});
res.json({ success: true, privateKey });
}),
);
// Admin: Get user's wallet
router.get(
"/admin/:userId",
authenticate,
@@ -191,9 +154,14 @@ router.get(
select: {
id: true,
plexUsername: true,
email: true,
isAdmin: true,
isActive: true,
walletAddress: true,
totalEarned: true,
totalSpent: true,
watchTimeMinutes: true,
createdAt: true,
},
});
@@ -201,13 +169,55 @@ router.get(
return res.status(404).json({ error: "User not found" });
}
const dbBalance = user.totalEarned - user.totalSpent;
res.json({
...user,
balance: dbBalance,
balance: user.totalEarned - user.totalSpent,
});
}),
);
router.post(
"/admin/:userId/adjust",
authenticate,
requireAdmin,
asyncHandler(async (req: AuthenticatedRequest, res) => {
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.userId },
});
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const delta = Number(amount);
const type = delta > 0 ? "BONUS" : "ADJUSTMENT";
await prisma.transaction.create({
data: {
userId: user.id,
type,
amount: Math.abs(delta),
description: reason || "Admin adjustment",
contentTitle: reason || "Admin adjustment",
},
});
await prisma.user.update({
where: { id: user.id },
data: {
totalEarned: delta > 0 ? { increment: delta } : undefined,
totalSpent: delta < 0 ? { increment: Math.abs(delta) } : undefined,
},
});
res.json({ success: true });
}),
);
export { router as walletRouter };
+43 -110
View File
@@ -2,32 +2,26 @@ import crypto from "crypto";
import { Router } from "express";
import { io } from "../index";
import { asyncHandler } from "../middleware/errorHandler";
import { mintTokens } from "../services/solana";
import { prisma } from "../utils/prisma";
const router = Router();
const WEBHOOK_SECRET = process.env.TAUTULLI_WEBHOOK_SECRET || "";
// Verify webhook signature
function verifyWebhookSignature(payload: string, signature: string): boolean {
if (!WEBHOOK_SECRET) return true; // Skip verification if no secret set
if (!WEBHOOK_SECRET) return true;
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Tautulli webhook endpoint
router.post(
"/tautulli",
asyncHandler(async (req, res) => {
const webhookSignature = req.headers["x-tautulli-signature"] as string;
const payload = JSON.stringify(req.body);
// Verify signature if configured
if (
WEBHOOK_SECRET &&
webhookSignature &&
@@ -37,87 +31,55 @@ router.post(
}
const event = req.body;
// Only process watched events
if (event.action !== "watched") {
if (event.action !== "watched")
return res.json({ message: "Event type not processed" });
}
// Validate required fields
if (!event.user_id || !event.rating_key || !event.session_key) {
if (!event.user_id || !event.rating_key || !event.session_key)
return res.status(400).json({ error: "Missing required fields" });
}
// Find user by Plex ID
const user = await prisma.user.findUnique({
where: { plexId: event.user_id.toString() },
});
if (!user) {
console.log(`User not found for Plex ID: ${event.user_id}`);
return res.status(404).json({ error: "User not found" });
}
if (!user.walletAddress) {
console.log(`User ${user.plexUsername} has no wallet`);
if (!user) return res.status(404).json({ error: "User not found" });
if (!user.walletAddress)
return res.status(400).json({ error: "User has no wallet" });
}
// Check for duplicate events
const existing = await prisma.watchEvent.findUnique({
where: { sessionId: event.session_key.toString() },
});
if (existing) return res.json({ message: "Event already processed" });
if (existing) {
return res.json({ message: "Event already processed" });
}
// Get system settings
const settings = await prisma.systemSettings.findFirst();
const creditsPerMinute = settings?.creditsPerMinute || 2;
const minWatchPercent = settings?.minWatchPercent || 80;
const minWatchMinutes = settings?.minWatchMinutes || 5;
// Calculate watch duration
const watchDurationMinutes = Math.floor(
(event.stopped - event.started) / 60,
);
const percentComplete = event.percent_complete || 0;
// Validate minimum requirements
if (percentComplete < minWatchPercent) {
if (percentComplete < minWatchPercent)
return res.json({
message: "Watch percentage too low",
percentComplete,
required: minWatchPercent,
});
}
if (watchDurationMinutes < minWatchMinutes) {
if (watchDurationMinutes < minWatchMinutes)
return res.json({
message: "Watch duration too short",
watchDurationMinutes,
required: minWatchMinutes,
});
}
// Calculate credits
let creditsEarned = watchDurationMinutes * creditsPerMinute;
// Apply multipliers
if (settings?.newReleaseMultiplier && event.is_new) {
if (settings?.newReleaseMultiplier && event.is_new)
creditsEarned = Math.floor(
creditsEarned * Number(settings.newReleaseMultiplier),
);
}
if (settings?.bonusMultiplierActive) {
if (settings?.bonusMultiplierActive)
creditsEarned = Math.floor(
creditsEarned * Number(settings.bonusMultiplier),
);
}
// Create watch event record
const watchEvent = await prisma.watchEvent.create({
data: {
userId: user.id,
@@ -133,75 +95,46 @@ router.post(
},
});
// Mint tokens on Solana
const signature = await mintTokens(user.walletAddress, creditsEarned, {
sessionId: event.session_key.toString(),
contentTitle: event.title,
watchDurationMinutes,
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
type: "EARN",
amount: creditsEarned,
watchEventId: watchEvent.id,
description: `Watched ${event.title}`,
contentTitle: event.title,
},
});
if (signature) {
// Create transaction record
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
type: "EARN",
amount: creditsEarned,
watchEventId: watchEvent.id,
solanaSignature: signature,
description: `Watched ${event.title}`,
contentTitle: event.title,
},
});
await prisma.user.update({
where: { id: user.id },
data: {
totalEarned: { increment: creditsEarned },
watchTimeMinutes: { increment: watchDurationMinutes },
},
});
// Update user stats
await prisma.user.update({
where: { id: user.id },
data: {
totalEarned: { increment: creditsEarned },
watchTimeMinutes: { increment: watchDurationMinutes },
},
});
await prisma.watchEvent.update({
where: { id: watchEvent.id },
data: { isProcessed: true },
});
// Mark watch event as processed
await prisma.watchEvent.update({
where: { id: watchEvent.id },
data: { isProcessed: true },
});
// Emit real-time update via WebSocket
io.to(`user:${user.id}`).emit("credits_earned", {
io.to(`user:${user.id}`).emit("credits_earned", {
amount: creditsEarned,
title: event.title,
transaction: {
id: transaction.id,
type: "EARN",
amount: creditsEarned,
title: event.title,
transaction: {
id: transaction.id,
type: "EARN",
amount: creditsEarned,
contentTitle: event.title,
createdAt: transaction.createdAt,
},
});
contentTitle: event.title,
createdAt: transaction.createdAt,
},
});
res.json({
success: true,
creditsEarned,
solanaSignature: signature,
message: `Minted ${creditsEarned} COOP for watching ${event.title}`,
});
} else {
res.status(500).json({ error: "Failed to mint tokens" });
}
}),
);
// Test webhook endpoint
router.post(
"/test",
asyncHandler(async (req, res) => {
res.json({
message: "Webhook endpoint working",
timestamp: new Date().toISOString(),
body: req.body,
success: true,
creditsEarned,
message: `Credited ${creditsEarned} COOP for watching ${event.title}`,
});
}),
);
+2
View File
@@ -87,6 +87,8 @@ export const adminApi = {
updateUser: (id: string, data: any) => api.put(`/admin/users/${id}`, data),
grantBonus: (userId: string, amount: number, reason?: string) =>
api.post(`/admin/users/${userId}/bonus`, { amount, reason }),
adjustUser: (userId: string, amount: number, reason?: string) =>
api.post(`/wallet/admin/${userId}/adjust`, { amount, reason }),
getAnalytics: () => api.get("/admin/analytics"),
getAllTransactions: (page = 1, limit = 50) =>
api.get(`/transactions/admin/all?page=${page}&limit=${limit}`),