diff --git a/backend/src/index.ts b/backend/src/index.ts index 865d8f5..c089dbc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,78 +1,89 @@ -import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import morgan from 'morgan'; -import rateLimit from 'express-rate-limit'; -import { createServer } from 'http'; -import { Server } from 'socket.io'; -import dotenv from 'dotenv'; +import express from "express"; +import cors from "cors"; +import helmet from "helmet"; +import morgan from "morgan"; +import rateLimit from "express-rate-limit"; +import { createServer } from "http"; +import { Server } from "socket.io"; +import dotenv from "dotenv"; +import fs from "fs"; +import path from "path"; -dotenv.config(); +const envPath = + ["../.env", "../../.env", ".env"] + .map((p) => path.resolve(process.cwd(), p)) + .find((p) => fs.existsSync(p)) || ".env"; -import { prisma } from './utils/prisma'; -import { errorHandler } from './middleware/errorHandler'; -import { authRouter } from './routes/auth'; -import { userRouter } from './routes/users'; -import { walletRouter } from './routes/wallet'; -import { transactionsRouter } from './routes/transactions'; -import { adminRouter } from './routes/admin'; -import { tautulliRouter } from './routes/tautulli'; -import { overseerRouter } from './routes/overseer'; -import { webhookRouter } from './routes/webhooks'; -import { setupSocketHandlers } from './services/socket'; +dotenv.config({ path: envPath }); + +import { prisma } from "./utils/prisma"; +import { errorHandler } from "./middleware/errorHandler"; +import { authRouter } from "./routes/auth"; +import { userRouter } from "./routes/users"; +import { walletRouter } from "./routes/wallet"; +import { transactionsRouter } from "./routes/transactions"; +import { adminRouter } from "./routes/admin"; +import { tautulliRouter } from "./routes/tautulli"; +import { overseerRouter } from "./routes/overseer"; +import { webhookRouter } from "./routes/webhooks"; +import { setupSocketHandlers } from "./services/socket"; const app = express(); const httpServer = createServer(app); const allowedOrigins = [ - process.env.FRONTEND_URL, - 'https://coop.hobokenchicken.com', - 'http://172.20.1.238:3000' + process.env.FRONTEND_URL, + "https://coop.hobokenchicken.com", + "http://172.20.1.238:3000", ].filter(Boolean) as string[]; const io = new Server(httpServer, { - cors: { - origin: allowedOrigins, - credentials: true - } + cors: { + origin: allowedOrigins, + credentials: true, + }, }); // Security middleware -app.use(helmet({ - crossOriginResourcePolicy: { policy: "cross-origin" } -})); -app.use(cors({ - origin: allowedOrigins, - credentials: true -})); +app.use( + helmet({ + crossOriginResourcePolicy: { policy: "cross-origin" }, + }), +); +app.use( + cors({ + origin: allowedOrigins, + credentials: true, + }), +); // Rate limiting const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100 // limit each IP to 100 requests per windowMs + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs }); app.use(limiter); // Body parsing -app.use(express.json({ limit: '10mb' })); +app.use(express.json({ limit: "10mb" })); app.use(express.urlencoded({ extended: true })); // Logging -app.use(morgan('combined')); +app.use(morgan("combined")); // Health check -app.get('/health', (_req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); +app.get("/health", (_req, res) => { + res.json({ status: "ok", timestamp: new Date().toISOString() }); }); // API routes -app.use('/api/auth', authRouter); -app.use('/api/users', userRouter); -app.use('/api/wallet', walletRouter); -app.use('/api/transactions', transactionsRouter); -app.use('/api/admin', adminRouter); -app.use('/api/tautulli', tautulliRouter); -app.use('/api/overseer', overseerRouter); -app.use('/webhooks', webhookRouter); +app.use("/api/auth", authRouter); +app.use("/api/users", userRouter); +app.use("/api/wallet", walletRouter); +app.use("/api/transactions", transactionsRouter); +app.use("/api/admin", adminRouter); +app.use("/api/tautulli", tautulliRouter); +app.use("/api/overseer", overseerRouter); +app.use("/webhooks", webhookRouter); // Error handling app.use(errorHandler); @@ -84,49 +95,51 @@ setupSocketHandlers(io); const PORT = process.env.PORT || 3001; async function startServer() { - try { - // Connect to database - await prisma.$connect(); - console.log('✅ Connected to database'); - - // Ensure default settings exist - const settings = await prisma.systemSettings.findFirst(); - if (!settings) { - await prisma.systemSettings.create({ - data: { - id: 'default' - } - }); - console.log('✅ Created default system settings'); - } - - httpServer.listen(PORT, () => { - console.log(`🚀 Server running on port ${PORT}`); - console.log(`📊 API URL: ${process.env.API_URL || `http://localhost:${PORT}`}`); - }); - } catch (error) { - console.error('Failed to start server:', error); - process.exit(1); - } + try { + // Connect to database + await prisma.$connect(); + console.log("✅ Connected to database"); + + // Ensure default settings exist + const settings = await prisma.systemSettings.findFirst(); + if (!settings) { + await prisma.systemSettings.create({ + data: { + id: "default", + }, + }); + console.log("✅ Created default system settings"); + } + + httpServer.listen(PORT, () => { + console.log(`🚀 Server running on port ${PORT}`); + console.log( + `📊 API URL: ${process.env.API_URL || `http://localhost:${PORT}`}`, + ); + }); + } catch (error) { + console.error("Failed to start server:", error); + process.exit(1); + } } // Graceful shutdown -process.on('SIGTERM', async () => { - console.log('SIGTERM received, shutting down gracefully'); - await prisma.$disconnect(); - httpServer.close(() => { - console.log('Server closed'); - process.exit(0); - }); +process.on("SIGTERM", async () => { + console.log("SIGTERM received, shutting down gracefully"); + await prisma.$disconnect(); + httpServer.close(() => { + console.log("Server closed"); + process.exit(0); + }); }); -process.on('SIGINT', async () => { - console.log('SIGINT received, shutting down gracefully'); - await prisma.$disconnect(); - httpServer.close(() => { - console.log('Server closed'); - process.exit(0); - }); +process.on("SIGINT", async () => { + console.log("SIGINT received, shutting down gracefully"); + await prisma.$disconnect(); + httpServer.close(() => { + console.log("Server closed"); + process.exit(0); + }); }); startServer(); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index de6b438..5b4fc65 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,192 +1,211 @@ -import { Router } from 'express'; -import axios from 'axios'; -import jwt from 'jsonwebtoken'; -import { prisma } from '../utils/prisma'; -import { asyncHandler } from '../middleware/errorHandler'; +import { Router } from "express"; +import axios from "axios"; +import jwt from "jsonwebtoken"; +import { prisma } from "../utils/prisma"; +import { asyncHandler } from "../middleware/errorHandler"; const router = Router(); -// Plex OAuth configuration -const PLEX_CLIENT_ID = process.env.PLEX_CLIENT_ID || ''; -const PLEX_CLIENT_SECRET = process.env.PLEX_CLIENT_SECRET || ''; -const PLEX_REDIRECT_URI = process.env.PLEX_REDIRECT_URI || ''; -const JWT_SECRET = process.env.JWT_SECRET || 'secret'; +const PLEX_CLIENT_ID = process.env.PLEX_CLIENT_ID || ""; +const PLEX_REDIRECT_URI = process.env.PLEX_REDIRECT_URI || ""; +const JWT_SECRET = process.env.JWT_SECRET || "secret"; -// Step 1: Get Plex OAuth URL -router.get('/plex/url', asyncHandler(async (_req, res) => { - const params = new URLSearchParams({ - client_id: PLEX_CLIENT_ID, - redirect_uri: PLEX_REDIRECT_URI, - response_type: 'code', - scope: 'openid profile', - 'X-Plex-Product': 'CoopCoins', - 'X-Plex-Client-Identifier': PLEX_CLIENT_ID, - 'X-Plex-Device': 'Web Browser', - 'X-Plex-Platform': 'Web' - }); - - const authUrl = `https://app.plex.tv/auth#?${params.toString()}`; - - res.json({ authUrl }); -})); +if (!PLEX_CLIENT_ID) { + console.error("ERROR: PLEX_CLIENT_ID not set"); +} -// Step 2: Handle Plex OAuth callback -router.post('/plex/callback', asyncHandler(async (req, res) => { - const { code } = req.body; - - if (!code) { - return res.status(400).json({ error: 'Authorization code required' }); - } - - // Exchange code for Plex token - const tokenResponse = await axios.post( - 'https://plex.tv/api/v2/oauth/token', - { - code, - client_id: PLEX_CLIENT_ID, - client_secret: PLEX_CLIENT_SECRET, - grant_type: 'authorization_code' - }, - { - headers: { - 'Accept': 'application/json', - 'X-Plex-Client-Identifier': PLEX_CLIENT_ID - } - } - ); - - const plexToken = tokenResponse.data.access_token; - - // Get user info from Plex - const userResponse = await axios.get('https://plex.tv/api/v2/user', { - headers: { - 'X-Plex-Token': plexToken, - 'X-Plex-Client-Identifier': PLEX_CLIENT_ID, - 'Accept': 'application/json' - } - }); - - const plexUser = userResponse.data; - - // Check if user exists, create if not - let user = await prisma.user.findUnique({ - where: { plexId: plexUser.id } - }); - - if (!user) { - user = await prisma.user.create({ - data: { - plexId: plexUser.id, - plexUsername: plexUser.username || plexUser.email, - email: plexUser.email, - // Check if user is Plex admin (implement your logic) - isAdmin: false - } - }); - } else { - // Update user info - user = await prisma.user.update({ - where: { id: user.id }, - data: { - plexUsername: plexUser.username || plexUser.email, - email: plexUser.email - } - }); - } - - // Create session - const session = await prisma.session.create({ - data: { - userId: user.id, - token: jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' }), - expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) - } - }); - - // Generate JWT - const token = jwt.sign( - { - userId: user.id, - plexId: user.plexId, - isAdmin: user.isAdmin - }, - JWT_SECRET, - { expiresIn: '7d' } - ); - - res.json({ - token, - sessionToken: session.token, - user: { - id: user.id, - plexId: user.plexId, - plexUsername: user.plexUsername, - email: user.email, - isAdmin: user.isAdmin, - walletAddress: user.walletAddress, - totalEarned: user.totalEarned, - totalSpent: user.totalSpent - } - }); -})); +// Step 1: Generate Plex PIN and return auth URL +router.get( + "/plex/url", + asyncHandler(async (_req, res) => { + if (!PLEX_CLIENT_ID) { + return res.status(500).json({ error: "Plex not configured" }); + } -// Verify token -router.get('/verify', asyncHandler(async (req, res) => { - const authHeader = req.headers.authorization; - - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'No token provided' }); - } - - 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 } - }); - - if (!user || !user.isActive) { - return res.status(401).json({ error: 'User not found or inactive' }); - } - - res.json({ - user: { - id: user.id, - plexId: user.plexId, - plexUsername: user.plexUsername, - email: user.email, - isAdmin: user.isAdmin, - walletAddress: user.walletAddress, - totalEarned: user.totalEarned, - totalSpent: user.totalSpent - } - }); - } catch (error) { - res.status(401).json({ error: 'Invalid token' }); - } -})); + const pinResponse = await axios.post( + "https://plex.tv/api/v2/pins?strong=true", + null, + { + headers: { + Accept: "application/json", + "X-Plex-Client-Identifier": PLEX_CLIENT_ID, + "X-Plex-Product": "CoopCoins", + "X-Plex-Version": "1.0.0", + "X-Plex-Device": "Web Browser", + "X-Plex-Platform": "Web", + }, + }, + ); -// Logout -router.post('/logout', asyncHandler(async (req, res) => { - const authHeader = req.headers.authorization; - - if (authHeader?.startsWith('Bearer ')) { - const token = authHeader.substring(7); - - try { - const decoded = jwt.verify(token, JWT_SECRET) as any; - - await prisma.session.deleteMany({ - where: { userId: decoded.userId } - }); - } catch { - // Invalid token, ignore - } - } - - res.json({ message: 'Logged out successfully' }); -})); + const pin = pinResponse.data; + const authUrl = `https://app.plex.tv/auth#?clientID=${encodeURIComponent(PLEX_CLIENT_ID)}&code=${pin.code}&forwardUrl=${encodeURIComponent(PLEX_REDIRECT_URI)}`; + + res.json({ authUrl, pinId: pin.id }); + }), +); + +// Step 2: Exchange PIN for Plex token and create user session +router.post( + "/plex/callback", + asyncHandler(async (req, res) => { + const { pinId } = req.body; + + if (!pinId) { + return res.status(400).json({ error: "PIN ID required" }); + } + + // Check PIN status to get auth token + const pinResponse = await axios.get( + `https://plex.tv/api/v2/pins/${pinId}`, + { + headers: { + Accept: "application/json", + "X-Plex-Client-Identifier": PLEX_CLIENT_ID, + }, + }, + ); + + const pin = pinResponse.data; + + if (!pin.authToken) { + return res.status(400).json({ error: "Authentication not completed" }); + } + + const plexToken = pin.authToken; + + // Get user info from Plex + const userResponse = await axios.get("https://plex.tv/api/v2/user", { + headers: { + "X-Plex-Token": plexToken, + "X-Plex-Client-Identifier": PLEX_CLIENT_ID, + Accept: "application/json", + }, + }); + + const plexUser = userResponse.data; + + // Find or create user + let user = await prisma.user.findUnique({ + where: { plexId: plexUser.id }, + }); + + if (!user) { + user = await prisma.user.create({ + data: { + plexId: plexUser.id, + plexUsername: plexUser.username || plexUser.email, + email: plexUser.email, + isAdmin: false, + }, + }); + } else { + user = await prisma.user.update({ + where: { id: user.id }, + data: { + plexUsername: plexUser.username || plexUser.email, + email: plexUser.email, + }, + }); + } + + // Create session + const session = await prisma.session.create({ + data: { + userId: user.id, + token: jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "7d" }), + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + + // Generate JWT + const token = jwt.sign( + { + userId: user.id, + plexId: user.plexId, + isAdmin: user.isAdmin, + }, + JWT_SECRET, + { expiresIn: "7d" }, + ); + + res.json({ + token, + sessionToken: session.token, + user: { + id: user.id, + plexId: user.plexId, + plexUsername: user.plexUsername, + email: user.email, + isAdmin: user.isAdmin, + walletAddress: user.walletAddress, + totalEarned: user.totalEarned, + totalSpent: user.totalSpent, + }, + }); + }), +); + +router.get( + "/verify", + asyncHandler(async (req, res) => { + const authHeader = req.headers.authorization; + + if (!authHeader?.startsWith("Bearer ")) { + return res.status(401).json({ error: "No token provided" }); + } + + 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 }, + }); + + if (!user || !user.isActive) { + return res.status(401).json({ error: "User not found or inactive" }); + } + + res.json({ + user: { + id: user.id, + plexId: user.plexId, + plexUsername: user.plexUsername, + email: user.email, + isAdmin: user.isAdmin, + walletAddress: user.walletAddress, + totalEarned: user.totalEarned, + totalSpent: user.totalSpent, + }, + }); + } catch (error) { + res.status(401).json({ error: "Invalid token" }); + } + }), +); + +router.post( + "/logout", + asyncHandler(async (req, res) => { + const authHeader = req.headers.authorization; + + if (authHeader?.startsWith("Bearer ")) { + const token = authHeader.substring(7); + + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + + await prisma.session.deleteMany({ + where: { userId: decoded.userId }, + }); + } catch { + // Ignore invalid tokens on logout + } + } + + res.json({ message: "Logged out successfully" }); + }), +); export { router as authRouter }; diff --git a/frontend/src/app/auth/callback/page.tsx b/frontend/src/app/auth/callback/page.tsx index 49bb9e9..418365d 100644 --- a/frontend/src/app/auth/callback/page.tsx +++ b/frontend/src/app/auth/callback/page.tsx @@ -1,55 +1,56 @@ -'use client'; +"use client"; -import { useEffect, useState } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { authApi } from '@/lib/api'; -import { useStore } from '@/lib/store'; -import { Loader2 } from 'lucide-react'; -import { toast } from 'sonner'; +import { useEffect, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { authApi } from "@/lib/api"; +import { useStore } from "@/lib/store"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; export default function AuthCallbackPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const { setUser, setToken } = useStore(); - const [status, setStatus] = useState('Completing sign in...'); + const router = useRouter(); + const searchParams = useSearchParams(); + const { setUser, setToken } = useStore(); + const [status, setStatus] = useState("Completing sign in..."); - useEffect(() => { - const code = searchParams.get('code'); - - if (!code) { - toast.error('Invalid authentication response'); - router.push('/login'); - return; - } + useEffect(() => { + const pinId = sessionStorage.getItem("plexPinId"); - const handleCallback = async () => { - try { - const response = await authApi.plexCallback(code); - const { user, token } = response.data; - - localStorage.setItem('token', token); - setUser(user); - setToken(token); - - toast.success(`Welcome, ${user.plexUsername}!`); - router.push('/dashboard'); - } catch (error) { - setStatus('Authentication failed'); - toast.error('Authentication failed'); - console.error(error); - setTimeout(() => router.push('/login'), 2000); - } - }; + if (!pinId) { + toast.error("Invalid authentication response"); + router.push("/login"); + return; + } - handleCallback(); - }, [searchParams, router, setUser, setToken]); + const handleCallback = async () => { + try { + const response = await authApi.plexCallback(pinId); + const { user, token } = response.data; - return ( -
-
- -

{status}

-
-
- ); + sessionStorage.removeItem("plexPinId"); + localStorage.setItem("token", token); + setUser(user); + setToken(token); + + toast.success(`Welcome, ${user.plexUsername}!`); + router.push("/dashboard"); + } catch (error) { + setStatus("Authentication failed"); + toast.error("Authentication failed"); + console.error(error); + setTimeout(() => router.push("/login"), 2000); + } + }; + + handleCallback(); + }, [searchParams, router, setUser, setToken]); + + return ( +
+
+ +

{status}

+
+
+ ); } diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 8a176ae..2fa80b0 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,93 +1,100 @@ -'use client'; +"use client"; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { authApi } from '@/lib/api'; -import { useStore } from '@/lib/store'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Loader2, Tv } from 'lucide-react'; -import { toast } from 'sonner'; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { authApi } from "@/lib/api"; +import { useStore } from "@/lib/store"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Loader2, Tv } from "lucide-react"; +import { toast } from "sonner"; export default function LoginPage() { - const router = useRouter(); - const { isAuthenticated } = useStore(); - const [isLoading, setIsLoading] = useState(false); - const [isMounted, setIsMounted] = useState(false); + const router = useRouter(); + const { isAuthenticated } = useStore(); + const [isLoading, setIsLoading] = useState(false); + const [isMounted, setIsMounted] = useState(false); - useEffect(() => { - setIsMounted(true); - }, []); + useEffect(() => { + setIsMounted(true); + }, []); - useEffect(() => { - if (isAuthenticated) { - router.push('/dashboard'); - } - }, [isAuthenticated, router]); + useEffect(() => { + if (isAuthenticated) { + router.push("/dashboard"); + } + }, [isAuthenticated, router]); - const handlePlexLogin = async () => { - setIsLoading(true); - try { - const response = await authApi.getPlexUrl(); - window.location.href = response.data.authUrl; - } catch (error) { - toast.error('Failed to initiate Plex login'); - console.error(error); - setIsLoading(false); - } - }; + const handlePlexLogin = async () => { + setIsLoading(true); + try { + const response = await authApi.getPlexUrl(); + sessionStorage.setItem("plexPinId", response.data.pinId); + window.location.href = response.data.authUrl; + } catch (error) { + toast.error("Failed to initiate Plex login"); + console.error(error); + setIsLoading(false); + } + }; - if (!isMounted) { - return ( -
- - -
- -
- CoopCoins - Loading... -
-
-
- ); - } + if (!isMounted) { + return ( +
+ + +
+ +
+ CoopCoins + Loading... +
+
+
+ ); + } - return ( -
- - -
- -
- CoopCoins - - Earn CoopCoins ($COOP) for watching content on Plex - -
- - -

- Sign in with your Plex account to start earning rewards -

-
-
-
- ); + return ( +
+ + +
+ +
+ CoopCoins + + Earn CoopCoins ($COOP) for watching content on Plex + +
+ + +

+ Sign in with your Plex account to start earning rewards +

+
+
+
+ ); } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 4906833..8eeab50 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,100 +1,115 @@ -import axios from 'axios'; +import axios from "axios"; -const API_URL = process.env.NEXT_PUBLIC_API_URL || (typeof window !== 'undefined' ? `${window.location.origin}/api` : 'http://localhost:3001/api'); +const API_URL = + process.env.NEXT_PUBLIC_API_URL || + (typeof window !== "undefined" + ? `${window.location.origin}/api` + : "http://localhost:3001/api"); export const api = axios.create({ - baseURL: `${API_URL}/api`, - headers: { - 'Content-Type': 'application/json', - }, + baseURL: `${API_URL}/api`, + headers: { + "Content-Type": "application/json", + }, }); // Add auth token to requests api.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; }); // Handle token expiration api.interceptors.response.use( - (response) => response, - (error) => { - if (error.response?.status === 401) { - localStorage.removeItem('token'); - localStorage.removeItem('user'); - window.location.href = '/login'; - } - return Promise.reject(error); - } + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + window.location.href = "/login"; + } + return Promise.reject(error); + }, ); // Auth API export const authApi = { - getPlexUrl: () => api.get('/auth/plex/url'), - plexCallback: (code: string) => api.post('/auth/plex/callback', { code }), - verify: () => api.get('/auth/verify'), - logout: () => api.post('/auth/logout'), + getPlexUrl: () => api.get("/auth/plex/url"), + plexCallback: (pinId: string) => api.post("/auth/plex/callback", { pinId }), + verify: () => api.get("/auth/verify"), + logout: () => api.post("/auth/logout"), }; // User API export const userApi = { - getMe: () => api.get('/users/me'), - updateMe: (data: { email?: string }) => api.put('/users/me', data), - getWatchHistory: (page = 1, limit = 20) => - api.get(`/users/me/watch-history?page=${page}&limit=${limit}`), - getRequests: (page = 1, limit = 20, status?: string) => - api.get(`/users/me/requests?page=${page}&limit=${limit}${status ? `&status=${status}` : ''}`), - getLeaderboard: () => api.get('/users/leaderboard'), + getMe: () => api.get("/users/me"), + updateMe: (data: { email?: string }) => api.put("/users/me", data), + getWatchHistory: (page = 1, limit = 20) => + api.get(`/users/me/watch-history?page=${page}&limit=${limit}`), + getRequests: (page = 1, limit = 20, status?: string) => + api.get( + `/users/me/requests?page=${page}&limit=${limit}${status ? `&status=${status}` : ""}`, + ), + getLeaderboard: () => api.get("/users/leaderboard"), }; // Wallet API export const walletApi = { - getWallet: () => api.get('/wallet'), - createWallet: () => api.post('/wallet/create'), - connectWallet: (address: string) => api.post('/wallet/connect', { address }), - backupWallet: () => api.post('/wallet/backup'), + getWallet: () => api.get("/wallet"), + createWallet: () => api.post("/wallet/create"), + connectWallet: (address: string) => api.post("/wallet/connect", { address }), + backupWallet: () => api.post("/wallet/backup"), }; // Transactions API export const transactionApi = { - getTransactions: (page = 1, limit = 20, type?: string) => - api.get(`/transactions?page=${page}&limit=${limit}${type ? `&type=${type}` : ''}`), - getStats: () => api.get('/transactions/stats'), - getRecentActivity: () => api.get('/transactions/recent'), + getTransactions: (page = 1, limit = 20, type?: string) => + api.get( + `/transactions?page=${page}&limit=${limit}${type ? `&type=${type}` : ""}`, + ), + getStats: () => api.get("/transactions/stats"), + getRecentActivity: () => api.get("/transactions/recent"), }; // Admin API export const adminApi = { - getSettings: () => api.get('/admin/settings'), - updateSettings: (data: any) => api.put('/admin/settings', data), - pause: () => api.post('/admin/pause'), - resume: () => api.post('/admin/resume'), - getUsers: (page = 1, limit = 50, search?: string) => - api.get(`/admin/users?page=${page}&limit=${limit}${search ? `&search=${search}` : ''}`), - 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 }), - getAnalytics: () => api.get('/admin/analytics'), - getAllTransactions: (page = 1, limit = 50) => - api.get(`/transactions/admin/all?page=${page}&limit=${limit}`), - getSystemStats: () => api.get('/transactions/admin/stats'), + getSettings: () => api.get("/admin/settings"), + updateSettings: (data: any) => api.put("/admin/settings", data), + pause: () => api.post("/admin/pause"), + resume: () => api.post("/admin/resume"), + getUsers: (page = 1, limit = 50, search?: string) => + api.get( + `/admin/users?page=${page}&limit=${limit}${search ? `&search=${search}` : ""}`, + ), + 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 }), + getAnalytics: () => api.get("/admin/analytics"), + getAllTransactions: (page = 1, limit = 50) => + api.get(`/transactions/admin/all?page=${page}&limit=${limit}`), + getSystemStats: () => api.get("/transactions/admin/stats"), }; // Overseer API export const overseerApi = { - getCosts: () => api.get('/overseer/costs'), - getBalance: () => api.get('/overseer/balance'), - search: (query: string) => api.get(`/overseer/search?query=${encodeURIComponent(query)}`), - request: (data: { mediaType: string; mediaId: number; title: string; seasons?: number[] }) => - api.post('/overseer/request', data), + getCosts: () => api.get("/overseer/costs"), + getBalance: () => api.get("/overseer/balance"), + search: (query: string) => + api.get(`/overseer/search?query=${encodeURIComponent(query)}`), + request: (data: { + mediaType: string; + mediaId: number; + title: string; + seasons?: number[]; + }) => api.post("/overseer/request", data), }; // Tautulli API export const tautulliApi = { - getStatus: () => api.get('/tautulli/status'), - getStats: () => api.get('/tautulli/stats'), - getWebhookConfig: () => api.get('/tautulli/webhook-config'), + getStatus: () => api.get("/tautulli/status"), + getStats: () => api.get("/tautulli/stats"), + getWebhookConfig: () => api.get("/tautulli/webhook-config"), };