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:
2026-04-14 11:09:50 -04:00
parent f106328f6a
commit e8d9b1fd42
69 changed files with 11382 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma';
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
export interface AuthenticatedRequest extends Request {
user?: {
id: string;
plexId: string;
isAdmin: boolean;
};
}
export const authenticate = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET) as any;
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, plexId: true, isAdmin: true, isActive: true }
});
if (!user || !user.isActive) {
return res.status(401).json({ error: 'User not found or inactive' });
}
req.user = {
id: user.id,
plexId: user.plexId,
isAdmin: user.isAdmin
};
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
};
export const requireAdmin = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
if (!req.user?.isAdmin) {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};
+32
View File
@@ -0,0 +1,32 @@
import { Request, Response, NextFunction } from 'express';
export interface ApiError extends Error {
statusCode?: number;
code?: string;
}
export const errorHandler = (
err: ApiError,
_req: Request,
res: Response,
_next: NextFunction
) => {
console.error('Error:', err);
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
error: {
message,
code: err.code || 'INTERNAL_ERROR',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
};
export const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};