- Sponsors table, LNbits createInvoice, webhook handler - Sponsor routes: create, homepage, list, my-ads, click, extend, check-payment - Admin routes for sponsor management - Frontend: SponsorForm, SponsorTimeSlider, SponsorCard, SponsorsSection - Sponsors page, My Ads page, homepage sponsor block - Header login dropdown with My Ads, Create Sponsor - Transactions integration for sponsor payments - View/click tracking - OG meta fetch for sponsor images - Sponsor modal spacing, invoice polling fallback - Remove Lightning address and Category fields from sponsor form Made-with: Cursor
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
import { Router, Request, Response, NextFunction } from "express";
|
|
import { config } from "../config.js";
|
|
import { getDb } from "../db/index.js";
|
|
import { verifyJwt } from "../auth/jwt.js";
|
|
|
|
const router = Router();
|
|
|
|
function adminAuth(req: Request, res: Response, next: NextFunction): void {
|
|
if (config.adminPubkeys.length === 0) {
|
|
res.status(503).json({ code: "admin_disabled", message: "Admin API not configured" });
|
|
return;
|
|
}
|
|
const auth = req.headers.authorization;
|
|
const adminKey = req.headers["x-admin-key"];
|
|
let pubkey: string | null = null;
|
|
|
|
if (auth?.startsWith("Bearer ")) {
|
|
const token = auth.slice(7).trim();
|
|
const payload = verifyJwt(token);
|
|
if (payload?.pubkey) pubkey = payload.pubkey;
|
|
}
|
|
if (!pubkey && typeof adminKey === "string") {
|
|
pubkey = adminKey.trim();
|
|
}
|
|
if (!pubkey || !config.adminPubkeys.includes(pubkey)) {
|
|
res.status(403).json({ code: "forbidden", message: "Admin access required" });
|
|
return;
|
|
}
|
|
next();
|
|
}
|
|
|
|
router.use(adminAuth);
|
|
|
|
router.get("/sponsors", async (req: Request, res: Response) => {
|
|
const db = getDb();
|
|
const status = typeof req.query.status === "string" ? req.query.status : undefined;
|
|
const limit = Math.min(parseInt(String(req.query.limit || 100), 10) || 100, 500);
|
|
|
|
const sponsors = await db.getAllSponsors({ status, limit });
|
|
|
|
res.json(sponsors);
|
|
});
|
|
|
|
router.patch("/sponsors/:id", async (req: Request, res: Response) => {
|
|
const id = parseInt(req.params.id, 10);
|
|
if (!Number.isFinite(id) || id < 1) {
|
|
res.status(400).json({ code: "invalid_id", message: "Invalid sponsor id" });
|
|
return;
|
|
}
|
|
const action = typeof (req.body as { action?: string }).action === "string"
|
|
? (req.body as { action: string }).action
|
|
: "";
|
|
const durationDays = typeof (req.body as { duration_days?: number }).duration_days === "number"
|
|
? (req.body as { duration_days: number }).duration_days
|
|
: undefined;
|
|
|
|
const db = getDb();
|
|
const sponsor = await db.getSponsorById(id);
|
|
if (!sponsor) {
|
|
res.status(404).json({ code: "not_found", message: "Sponsor not found" });
|
|
return;
|
|
}
|
|
|
|
switch (action) {
|
|
case "approve":
|
|
await db.updateSponsorStatus(id, "active");
|
|
if (!sponsor.activated_at || !sponsor.expires_at) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const expiresAt = now + sponsor.duration_days * 86400;
|
|
await db.updateSponsorActivation(id, now, expiresAt);
|
|
}
|
|
break;
|
|
case "reject":
|
|
await db.updateSponsorStatus(id, "removed");
|
|
break;
|
|
case "pause":
|
|
await db.updateSponsorStatus(id, "pending_review");
|
|
break;
|
|
case "remove":
|
|
await db.updateSponsorStatus(id, "removed");
|
|
break;
|
|
case "extend":
|
|
if (durationDays && durationDays >= 1 && durationDays <= 365) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const current = sponsor.expires_at ?? now;
|
|
const newExpires = Math.max(current, now) + durationDays * 86400;
|
|
await db.updateSponsorExpiresAt(id, newExpires);
|
|
} else {
|
|
res.status(400).json({ code: "invalid_duration", message: "duration_days 1-365 required" });
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
res.status(400).json({ code: "invalid_action", message: "action must be approve, reject, pause, remove, or extend" });
|
|
return;
|
|
}
|
|
|
|
const updated = await db.getSponsorById(id);
|
|
res.json(updated);
|
|
});
|
|
|
|
router.delete("/sponsors/:id", async (req: Request, res: Response) => {
|
|
const id = parseInt(req.params.id, 10);
|
|
if (!Number.isFinite(id) || id < 1) {
|
|
res.status(400).json({ code: "invalid_id", message: "Invalid sponsor id" });
|
|
return;
|
|
}
|
|
const db = getDb();
|
|
const sponsor = await db.getSponsorById(id);
|
|
if (!sponsor) {
|
|
res.status(404).json({ code: "not_found", message: "Sponsor not found" });
|
|
return;
|
|
}
|
|
await db.updateSponsorStatus(id, "removed");
|
|
res.status(200).json({ ok: true });
|
|
});
|
|
|
|
export default router;
|