import axios from "axios"; import { Router } from "express"; import jwt from "jsonwebtoken"; import { asyncHandler } from "../middleware/errorHandler"; import { prisma } from "../utils/prisma"; import { backfillUserHistory } from "../services/tautulli"; const router = Router(); 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"; router.get( "/plex/url", asyncHandler(async (_req, res) => { if (!PLEX_CLIENT_ID) return res.status(500).json({ error: "Plex not configured" }); 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", }, }, ); 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 }); }), ); router.post( "/plex/callback", asyncHandler(async (req, res) => { const { pinId } = req.body; if (!pinId) return res.status(400).json({ error: "PIN ID required" }); 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 userResponse = await axios.get("https://plex.tv/api/v2/user", { headers: { "X-Plex-Token": pin.authToken, "X-Plex-Client-Identifier": PLEX_CLIENT_ID, Accept: "application/json", }, }); const plexUser = userResponse.data; let user = await prisma.user.findUnique({ where: { plexId: String(plexUser.id) }, }); const isNewUser = !user; if (!user) { user = await prisma.user.create({ data: { plexId: String(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, }, }); } await prisma.session.deleteMany({ where: { userId: user.id } }); const sessionToken = jwt.sign( { userId: user.id, nonce: Date.now() }, JWT_SECRET, { expiresIn: "7d" }, ); const session = await prisma.session.create({ data: { userId: user.id, token: sessionToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, }); if (isNewUser) await backfillUserHistory(user); 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, 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" }); try { const decoded = jwt.verify(authHeader.substring(7), 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, totalEarned: user.totalEarned, totalSpent: user.totalSpent, }, }); } catch { res.status(401).json({ error: "Invalid token" }); } }), ); router.post( "/logout", asyncHandler(async (req, res) => { const authHeader = req.headers.authorization; if (authHeader?.startsWith("Bearer ")) { try { const decoded = jwt.verify(authHeader.substring(7), JWT_SECRET) as any; await prisma.session.deleteMany({ where: { userId: decoded.userId } }); } catch {} } res.json({ message: "Logged out successfully" }); }), ); export { router as authRouter };