Production-ready overhaul: backend fixes, claim flow polish, SEO, mobile, nginx
Backend: - Fix activity score (followersCount check), NIP-98 URL proto, wallet balance guard - Add payment_hash to confirm response, spentTodaySats to stats - Add idx_claims_ip_hash; security headers, graceful shutdown, periodic nonce cleanup Frontend: - Remove 8 legacy components; polish wizard (rules summary, profile card, confetti, share) - Stats budget bar uses spentTodaySats; ErrorBoundary with reload SEO & production: - Full meta/OG/Twitter, favicon, JSON-LD, robots.txt, sitemap.xml - Mobile CSS fixes; nginx static dist + gzip + cache + security headers - Vite manualChunks; aria-labels, dynamic page titles Made-with: Cursor
This commit is contained in:
@@ -46,5 +46,6 @@ CREATE TABLE IF NOT EXISTS nonces (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_pubkey ON claims(pubkey);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_claimed_at ON claims(claimed_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_ip_hash ON claims(ip_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_quotes_expires_at ON quotes(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status);
|
||||
|
||||
@@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS deposits (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_pubkey ON claims(pubkey);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_claimed_at ON claims(claimed_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_ip_hash ON claims(ip_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_quotes_expires_at ON quotes(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_deposits_created_at ON deposits(created_at);
|
||||
|
||||
@@ -64,6 +64,7 @@ CREATE TABLE IF NOT EXISTS deposits (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_pubkey ON claims(pubkey);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_claimed_at ON claims(claimed_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_ip_hash ON claims(ip_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_quotes_expires_at ON quotes(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_deposits_created_at ON deposits(created_at);
|
||||
|
||||
@@ -9,6 +9,8 @@ import authRoutes from "./routes/auth.js";
|
||||
import claimRoutes from "./routes/claim.js";
|
||||
import userRoutes from "./routes/user.js";
|
||||
|
||||
const NONCE_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
async function main() {
|
||||
const db = getDb();
|
||||
await db.runMigrations();
|
||||
@@ -17,6 +19,14 @@ async function main() {
|
||||
const app = express();
|
||||
if (config.trustProxy) app.set("trust proxy", 1);
|
||||
app.use(express.json({ limit: "10kb" }));
|
||||
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
res.setHeader("X-Frame-Options", "DENY");
|
||||
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: (origin, cb) => {
|
||||
@@ -50,12 +60,34 @@ async function main() {
|
||||
userRoutes
|
||||
);
|
||||
|
||||
app.listen(config.port, () => {
|
||||
const server = app.listen(config.port, () => {
|
||||
console.log(`Faucet API listening on port ${config.port}`);
|
||||
if (config.lnbitsBaseUrl && config.lnbitsAdminKey) {
|
||||
startLnbitsDepositSync();
|
||||
}
|
||||
});
|
||||
|
||||
const nonceCleanupTimer = setInterval(() => {
|
||||
db.deleteExpiredNonces().catch((err) =>
|
||||
console.error("[nonce-cleanup] Failed:", err instanceof Error ? err.message : err)
|
||||
);
|
||||
}, NONCE_CLEANUP_INTERVAL_MS);
|
||||
|
||||
function shutdown(signal: string) {
|
||||
console.log(`\n[${signal}] Shutting down gracefully…`);
|
||||
clearInterval(nonceCleanupTimer);
|
||||
server.close(() => {
|
||||
console.log("[shutdown] HTTP server closed.");
|
||||
process.exit(0);
|
||||
});
|
||||
setTimeout(() => {
|
||||
console.error("[shutdown] Forceful exit after timeout.");
|
||||
process.exit(1);
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
@@ -83,8 +83,8 @@ export async function nip98Auth(req: Request, res: Response, next: NextFunction)
|
||||
}
|
||||
|
||||
// Reconstruct absolute URL (protocol + host + path + query)
|
||||
const proto = req.headers["x-forwarded-proto"] ?? (req.socket as { encrypted?: boolean }).encrypted ? "https" : "http";
|
||||
const host = req.headers["x-forwarded-host"] ?? req.headers.host ?? "";
|
||||
const proto = (req.headers["x-forwarded-proto"] as string | undefined) ?? ((req.socket as { encrypted?: boolean }).encrypted ? "https" : "http");
|
||||
const host = (req.headers["x-forwarded-host"] as string | undefined) ?? req.headers.host ?? "";
|
||||
const path = req.originalUrl ?? req.url;
|
||||
const absoluteUrl = `${proto}://${host}${path}`;
|
||||
if (u !== absoluteUrl) {
|
||||
|
||||
@@ -140,6 +140,7 @@ router.post("/confirm", authOrNip98, async (req: Request, res: Response) => {
|
||||
res.json({
|
||||
success: true,
|
||||
payout_sats: quote.payout_sats,
|
||||
payment_hash: paymentHash,
|
||||
next_eligible_at: cooldownEnd,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -24,11 +24,14 @@ router.get("/config", (_req: Request, res: Response) => {
|
||||
router.get("/stats", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const [balance, totalPaid, totalClaims, claims24h, recent, recentDeposits] = await Promise.all([
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const dayStart = now - (now % 86400);
|
||||
const [balance, totalPaid, totalClaims, claims24h, spentToday, recent, recentDeposits] = await Promise.all([
|
||||
getWalletBalanceSats().catch(() => 0),
|
||||
db.getTotalPaidSats(),
|
||||
db.getTotalClaimsCount(),
|
||||
db.getClaimsCountSince(Math.floor(Date.now() / 1000) - 86400),
|
||||
db.getClaimsCountSince(now - 86400),
|
||||
db.getPaidSatsSince(dayStart),
|
||||
db.getRecentPayouts(20),
|
||||
db.getRecentDeposits(20),
|
||||
]);
|
||||
@@ -38,6 +41,7 @@ router.get("/stats", async (_req: Request, res: Response) => {
|
||||
totalClaims,
|
||||
claimsLast24h: claims24h,
|
||||
dailyBudgetSats: config.dailyBudgetSats,
|
||||
spentTodaySats: spentToday,
|
||||
recentPayouts: recent,
|
||||
recentDeposits,
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function checkEligibility(pubkey: string, ipHash: string): Promise<
|
||||
};
|
||||
}
|
||||
|
||||
if (balanceSats < config.faucetMinSats) {
|
||||
if (balanceSats < config.minWalletBalanceSats) {
|
||||
return {
|
||||
eligible: false,
|
||||
denialCode: "insufficient_balance",
|
||||
|
||||
@@ -101,7 +101,8 @@ export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile =
|
||||
if (hasMetadata) score += 10;
|
||||
if (notesInLookback >= config.minNotesCount) score += 20;
|
||||
if (followingCount >= config.minFollowingCount) score += 10;
|
||||
if (0 >= config.minFollowersCount) score += 10; // followers not fetched for MVP; treat as 0
|
||||
const followersCount = 0; // followers not fetched for MVP
|
||||
if (followersCount >= config.minFollowersCount) score += 10;
|
||||
|
||||
let lightning_address: string | null = null;
|
||||
let name: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user