feat: Implement email/password and X OAuth authentication, replacing the dev-login mechanism.

This commit is contained in:
Codex
2026-02-18 14:54:28 +00:00
parent c92032eb72
commit 76f991e690
15 changed files with 410 additions and 147 deletions

View File

@@ -182,22 +182,31 @@ function renderLoginPage({ returnTo = "/app", error = null }) {
<label class="form-control w-full">
<span class="label-text text-slate-200 mb-1">Password</span>
<input name="password" type="password" required minlength="8" maxlength="128" class="input input-bordered w-full bg-slate-950" placeholder="••••••••" />
<form method="POST" action="/auth/dev-login" class="space-y-4">
</label>
<button class="btn btn-primary w-full">Sign in with email</button>
</form>
<div class="divider text-xs uppercase text-slate-400">Create account</div>
<form method="POST" action="/auth/email/sign-up" class="space-y-3">
<input type="hidden" name="returnTo" value="${escapeHtml(returnTo)}" />
<div class="form-control">
<label class="label">
<span class="label-text">Username</span>
</label>
<input name="userId" required minlength="2" maxlength="40" class="input input-bordered w-full" placeholder="matiss" />
</div>
<button class="btn btn-primary w-full">Continue</button>
<label class="form-control w-full">
<span class="label-text text-slate-200 mb-1">Name</span>
<input name="name" required minlength="2" maxlength="80" class="input input-bordered w-full bg-slate-950" placeholder="Matiss" />
</label>
<label class="form-control w-full">
<span class="label-text text-slate-200 mb-1">Email</span>
<input name="email" type="email" required class="input input-bordered w-full bg-slate-950" placeholder="you@domain.com" />
</label>
<label class="form-control w-full">
<span class="label-text text-slate-200 mb-1">Password</span>
<input name="password" type="password" required minlength="8" maxlength="128" class="input input-bordered w-full bg-slate-950" placeholder="••••••••" />
</label>
<button class="btn btn-outline w-full">Create account</button>
</form>
</div>
</div>
</section>
`,
});
}
function renderAppPage({ userId, summary, jobs, flash = null }) {
const flashMarkup = flash ? `<div role="alert" class="alert alert-info mb-4"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg><span>${escapeHtml(flash)}</span></div>` : "";
@@ -277,18 +286,18 @@ function renderAudioPage({ audio, accessDecision, userId, playbackUrl = null })
return shell({
title: "Audio not found",
compact: true,
content: `${nav({ authenticated: Boolean(userId), userId })}<div class="alert alert-error">Audio not found.</div>`,
content: `${nav({ authenticated: Boolean(userId), userId })}<div role="alert" class="alert alert-error"><span>Audio not found.</span></div>`,
});
}
const action = accessDecision.allowed
? `<div class="alert alert-success">Access granted. This unlock is permanent for your account.</div>`
? `<div role="alert" class="alert alert-success"><span>Access granted. This unlock is permanent for your account.</span></div>`
: accessDecision.reason === "auth_required"
? `<div class="alert alert-warning mb-3">Sign in to continue.</div>
<a href="/login?returnTo=/audio/${audio.id}" class="btn btn-primary">Sign in to unlock</a>`
: `<div class="alert alert-warning mb-3">Unlock required: ${accessDecision.creditsRequired} credits.</div>
? `<div role="alert" class="alert alert-warning mb-4"><span>Sign in to continue.</span></div>
<a href="/login?returnTo=/audio/${audio.id}" class="btn btn-primary w-full">Sign in to unlock</a>`
: `<div role="alert" class="alert alert-warning mb-4"><span>Unlock required: ${accessDecision.creditsRequired} credits.</span></div>
<form method="POST" action="/audio/${audio.id}/unlock">
<button class="btn btn-primary">Pay ${accessDecision.creditsRequired} credits and unlock forever</button>
<button class="btn btn-primary w-full">Pay ${accessDecision.creditsRequired} credits and unlock forever</button>
</form>`;
return shell({
@@ -296,16 +305,30 @@ function renderAudioPage({ audio, accessDecision, userId, playbackUrl = null })
compact: true,
content: `
${nav({ authenticated: Boolean(userId), userId })}
<section class="card bg-slate-900/85 border border-slate-700">
<div class="card-body p-4">
<h1 class="text-xl font-bold">${escapeHtml(audio.articleTitle)}</h1>
<div class="text-xs text-slate-400">Duration ~ ${audio.durationSec}s • Asset ${escapeHtml(audio.id)}</div>
<div class="card w-full max-w-2xl mx-auto shadow-2xl bg-base-100 border border-base-200">
<div class="card-body p-6">
<span class="badge badge-accent mb-2">Audiobook</span>
<h1 class="text-2xl sm:text-3xl font-bold mb-2 leading-tight">${escapeHtml(audio.articleTitle)}</h1>
<div class="text-sm text-base-content/60 mb-6 flex gap-3 text-mono">
<span>Duration ~ ${audio.durationSec}s</span>
<span>•</span>
<span>Asset ${escapeHtml(audio.id.substring(0, 8))}...</span>
</div>
<div class="divider"></div>
${action}
${accessDecision.allowed
? `<div class="mockup-code mt-3"><pre><code>${escapeHtml(playbackUrl || `stream://${audio.storageKey}`)}</code></pre></div>`
? `<div class="mt-6">
<h3 class="font-bold mb-2">Direct Stream URL</h3>
<div class="mockup-code bg-neutral text-neutral-content p-4 text-sm overflow-x-auto">
<pre><code>${escapeHtml(playbackUrl || `stream://${audio.storageKey}`)}</code></pre>
</div>
</div>`
: ""}
</div>
</section>
</div>
`,
});
}