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.
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user