Files
coop/backend/src/routes/admin.ts
T
hobokenchicken e8d9b1fd42 feat: add CoopCredits Solana media rewards ecosystem
Add complete  token system with Plex/Tautulli/Overseer integration:
- Anchor program for SPL token mint/burn/transfer
- Express backend with OAuth, webhooks, Solana integration
- Next.js frontend with dashboard, admin panel, wallet management
- Docker deployment for 172.20.1.0/24 infrastructure
- Production configs with SSL, Nginx, health monitoring

Tautulli webhooks auto-mint  on watch events.
Overseer integration burns  for content requests.
2026-04-14 11:09:50 -04:00

309 lines
7.2 KiB
TypeScript

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';
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);
})
);
// 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);
})
);
// 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' });
})
);
// 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' });
})
);
// 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;
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 })
]);
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);
})
);
// Grant bonus credits
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' });
}
const user = await prisma.user.findUnique({
where: { id: req.params.id }
});
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 }
}
});
res.json({
success: true,
amount,
solanaSignature: signature,
transaction
});
})
);
// Get dashboard analytics
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([
// 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
`
]);
res.json({
users: userStats[0],
transactions: transactionStats[0],
watchStats: watchStats[0],
dailyActivity
});
})
);
export { router as adminRouter };