feat: implement full landing, auth, dashboard, and browser unlock flows

This commit is contained in:
Codex
2026-02-18 12:56:12 +00:00
parent 2fa4f62632
commit 76f673fe4c
2 changed files with 352 additions and 192 deletions

View File

@@ -1,47 +1,51 @@
"use strict";
const { randomUUID } = require("node:crypto");
const { XArtAudioEngine } = require("./lib/engine");
const { verifySignature } = require("./lib/signature");
const { renderHomePage, renderAudioPage } = require("./views/pages");
const {
renderLandingPage,
renderLoginPage,
renderAppPage,
renderAudioPage,
} = require("./views/pages");
const {
json,
html,
text,
redirect,
parseJSON,
parseFormUrlEncoded,
withQuery,
} = require("./lib/http");
const {
getAuthenticatedUserId,
serializeUserCookie,
clearUserCookie,
} = require("./lib/auth");
function json(status, data) {
return {
status,
headers: { "content-type": "application/json; charset=utf-8" },
body: JSON.stringify(data),
};
}
function html(status, markup) {
return {
status,
headers: { "content-type": "text/html; charset=utf-8" },
body: markup,
};
}
function text(status, body, contentType) {
return {
status,
headers: { "content-type": contentType || "text/plain; charset=utf-8" },
body,
};
}
function parseJSON(rawBody) {
if (!rawBody) {
return {};
function sanitizeReturnTo(value, fallback = "/app") {
if (!value || typeof value !== "string") {
return fallback;
}
try {
return JSON.parse(rawBody);
} catch {
throw new Error("invalid_json");
if (!value.startsWith("/")) {
return fallback;
}
if (value.startsWith("//")) {
return fallback;
}
return value;
}
function getUserIdFromHeaders(headers) {
return headers["x-user-id"] || null;
function parsePositiveInt(raw, fallback = null) {
const parsed = Number.parseInt(String(raw || ""), 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
return fallback;
}
return parsed;
}
function buildApp({ config }) {
@@ -49,6 +53,17 @@ function buildApp({ config }) {
creditConfig: config.credit,
});
function ensureAuth(userId, returnTo) {
if (userId) {
return null;
}
return redirect(withQuery("/login", {
returnTo: sanitizeReturnTo(returnTo, "/app"),
error: "Please sign in first",
}));
}
function handleXWebhook(headers, rawBody) {
const signature = headers["x-signature"];
const isValid = verifySignature({
@@ -63,26 +78,30 @@ function buildApp({ config }) {
const payload = parseJSON(rawBody);
const result = engine.processMention({
mentionPostId: payload.mentionPostId,
callerUserId: payload.callerUserId,
parentPost: payload.parentPost,
});
if (!result.ok && result.status === "not_article") {
return json(200, {
status: "not_article",
message: "This parent post is not an X Article.",
try {
const result = engine.processMention({
mentionPostId: payload.mentionPostId,
callerUserId: payload.callerUserId,
parentPost: payload.parentPost,
});
}
return json(200, {
status: "completed",
deduped: result.deduped,
jobId: result.job.id,
creditsCharged: result.job.creditsCharged,
publicLink: result.reply ? result.reply.publicLink : `/audio/${result.job.assetId}`,
});
if (!result.ok && result.status === "not_article") {
return json(200, {
status: "not_article",
message: "This parent post is not an X Article.",
});
}
return json(200, {
status: "completed",
deduped: result.deduped,
jobId: result.job.id,
creditsCharged: result.job.creditsCharged,
publicLink: result.reply ? result.reply.publicLink : `/audio/${result.job.assetId}`,
});
} catch (error) {
return json(400, { error: error.message });
}
}
function handlePolarWebhook(headers, rawBody) {
@@ -98,23 +117,19 @@ function buildApp({ config }) {
}
const payload = parseJSON(rawBody);
engine.topUpCredits(payload.userId, payload.credits, `polar:${payload.eventId}`);
return json(200, { status: "credited" });
}
function getJobsForUser(userId) {
if (!userId) {
return [];
try {
engine.topUpCredits(payload.userId, payload.credits, `polar:${payload.eventId}`);
return json(200, { status: "credited" });
} catch (error) {
return json(400, { error: error.message });
}
return Array.from(engine.jobs.values())
.filter((job) => job.callerUserId === userId)
.sort((a, b) => b.id.localeCompare(a.id));
}
function handleRequest({ method, path, headers, rawBody }) {
function handleRequest({ method, path, headers, rawBody, query }) {
const safeHeaders = headers || {};
const userId = getUserIdFromHeaders(safeHeaders);
const safeQuery = query || {};
const userId = getAuthenticatedUserId(safeHeaders);
if (method === "GET" && path === "/health") {
return json(200, { ok: true });
@@ -128,8 +143,8 @@ function buildApp({ config }) {
short_name: "XArtAudio",
start_url: "/",
display: "standalone",
background_color: "#1d232a",
theme_color: "#1d232a",
background_color: "#0f172a",
theme_color: "#0f172a",
icons: [],
}),
"application/manifest+json; charset=utf-8",
@@ -145,14 +160,138 @@ function buildApp({ config }) {
}
if (method === "GET" && path === "/") {
return html(200, renderHomePage({
authenticated: Boolean(userId),
userId,
balance: userId ? engine.getWalletBalance(userId) : 0,
jobs: getJobsForUser(userId),
return html(200, renderLandingPage({ authenticated: Boolean(userId), userId }));
}
if (method === "GET" && path === "/login") {
if (userId) {
return redirect("/app");
}
return html(200, renderLoginPage({
returnTo: sanitizeReturnTo(safeQuery.returnTo, "/app"),
error: safeQuery.error || null,
}));
}
if (method === "POST" && path === "/auth/dev-login") {
const form = parseFormUrlEncoded(rawBody);
const requestedUserId = String(form.userId || "").trim();
if (!/^[a-zA-Z0-9_-]{2,40}$/.test(requestedUserId)) {
return redirect(withQuery("/login", {
returnTo: sanitizeReturnTo(form.returnTo, "/app"),
error: "Username must be 2-40 characters using letters, numbers, _ or -",
}));
}
const nextPath = sanitizeReturnTo(form.returnTo, "/app");
return redirect(nextPath, {
"set-cookie": serializeUserCookie(requestedUserId),
});
}
if (method === "POST" && path === "/auth/logout") {
return redirect("/", {
"set-cookie": clearUserCookie(),
});
}
if (method === "GET" && path === "/app") {
const authResponse = ensureAuth(userId, "/app");
if (authResponse) {
return authResponse;
}
return html(200, renderAppPage({
userId,
summary: engine.getUserSummary(userId),
jobs: engine.listJobsForUser(userId),
flash: safeQuery.flash || null,
}));
}
if (method === "POST" && path === "/app/actions/topup") {
const authResponse = ensureAuth(userId, "/app");
if (authResponse) {
return authResponse;
}
const form = parseFormUrlEncoded(rawBody);
const amount = parsePositiveInt(form.amount, null);
if (!amount || amount > 500) {
return redirect(withQuery("/app", { flash: "Invalid credit amount" }));
}
engine.topUpCredits(userId, amount, `app-topup:${userId}:${randomUUID()}`);
return redirect(withQuery("/app", { flash: `Added ${amount} credits` }));
}
if (method === "POST" && path === "/app/actions/simulate-mention") {
const authResponse = ensureAuth(userId, "/app");
if (authResponse) {
return authResponse;
}
const form = parseFormUrlEncoded(rawBody);
const title = String(form.title || "").trim();
const body = String(form.body || "").trim();
if (!title || !body) {
return redirect(withQuery("/app", { flash: "Title and body are required" }));
}
try {
const result = engine.processMention({
mentionPostId: `manual:${userId}:${randomUUID()}`,
callerUserId: userId,
parentPost: {
id: `manual-parent:${randomUUID()}`,
authorId: userId,
article: {
id: `manual-article:${randomUUID()}`,
title,
body,
},
},
});
if (!result.ok) {
return redirect(withQuery("/app", { flash: "Parent post is not an article" }));
}
return redirect(withQuery(`/audio/${result.job.assetId}`, {
flash: "Audiobook generated",
}));
} catch (error) {
return redirect(withQuery("/app", { flash: `Generation failed: ${error.message}` }));
}
}
if (method === "POST" && path.startsWith("/audio/") && path.endsWith("/unlock")) {
const assetId = path.slice("/audio/".length, -"/unlock".length);
const authResponse = ensureAuth(userId, `/audio/${assetId}`);
if (authResponse) {
return authResponse;
}
try {
engine.unlockAudio(assetId, userId);
return redirect(withQuery(`/audio/${assetId}`, { flash: "Unlocked" }));
} catch (error) {
return redirect(withQuery(`/audio/${assetId}`, { flash: `Unlock failed: ${error.message}` }));
}
}
if (method === "GET" && path.startsWith("/audio/")) {
const assetId = path.slice("/audio/".length);
const audio = engine.getAsset(assetId);
const accessDecision = audio
? engine.checkAudioAccess(assetId, userId)
: { allowed: false, reason: "not_found" };
return html(200, renderAudioPage({ audio, accessDecision, userId }));
}
if (method === "POST" && path === "/api/webhooks/x") {
return handleXWebhook(safeHeaders, rawBody);
}
@@ -161,12 +300,6 @@ function buildApp({ config }) {
return handlePolarWebhook(safeHeaders, rawBody);
}
if (method === "POST" && path === "/api/dev/topup") {
const body = parseJSON(rawBody);
engine.topUpCredits(body.userId, body.amount, `dev:${Date.now()}:${body.userId}`);
return json(200, { status: "credited" });
}
if (method === "GET" && path === "/api/me/wallet") {
if (!userId) {
return json(401, { error: "auth_required" });
@@ -178,6 +311,7 @@ function buildApp({ config }) {
if (!userId) {
return json(401, { error: "auth_required" });
}
const jobId = path.slice("/api/jobs/".length);
const job = engine.getJob(jobId);
if (!job) {
@@ -202,15 +336,6 @@ function buildApp({ config }) {
}
}
if (method === "GET" && path.startsWith("/audio/")) {
const assetId = path.slice("/audio/".length);
const audio = engine.getAsset(assetId);
const accessDecision = audio
? engine.checkAudioAccess(assetId, userId)
: { allowed: false, reason: "not_found" };
return html(200, renderAudioPage({ audio, accessDecision }));
}
return json(404, { error: "not_found" });
}