diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 537bf6e..9a31168 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -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 }; diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 621d6dc..f1eefd6 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -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 diff --git a/backend/src/routes/wallet.ts b/backend/src/routes/wallet.ts index 18d1adf..340b033 100644 --- a/backend/src/routes/wallet.ts +++ b/backend/src/routes/wallet.ts @@ -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 }; diff --git a/backend/src/routes/webhooks.ts b/backend/src/routes/webhooks.ts index 7220ea4..80973f8 100644 --- a/backend/src/routes/webhooks.ts +++ b/backend/src/routes/webhooks.ts @@ -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}`, }); }), ); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 8eeab50..253172c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -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}`),