mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 06:47:28 +00:00
feat(pwa): logicsrc credentials app — real auth + Turso, redesigned; retire commandboard-api credshare
Adds apps/pwa: an Express + libSQL/Turso app that is now the home of team credential sharing, with the moshcode-style auth stack ported and reskinned to match logicsrc.com (light theme, Inter, green accent). apps/pwa - auth: email/password (scrypt), passkeys (WebAuthn), CoinPay OAuth, cookie sessions, and lsk_ API keys for the CLI via a loopback OAuth-PKCE flow (/cli/authorize + /cli/token). Ported from the moshcode PWA. - credshare API (/api/credshare/*): teams, members, invites, vaults, sealed grants, ciphertext secrets, audit — authed by session OR Bearer lsk_ key. Zero-knowledge: only ciphertext + sealed vault keys + public keys stored. - teams dashboard, accept-invite, and settings (API keys) pages, server-rendered in the LogicSRC brand (lib/html.mjs). - migrations (libSQL) 001_auth + 002_credshare, migrate-on-boot; Turso via TURSO_DATABASE_URL / TURSO_AUTH_TOKEN, or a local file db for dev. - trimmed moshcode-specific approvals/credits/push/deliver. CLI - `logicsrc login` now does browser loopback OAuth-PKCE against the app and stores an lsk_ token (email-OTP removed); --token for CI. Client repointed. Distribution - install.sh (served at logicsrc.com/install.sh) installs the CLI from the GitHub repo: tarball -> npm install -> `npm run build:cli` -> logicsrc wrapper. - root build:cli builds only the CLI's workspace chain (skips web/api/next). Cleanup - removed the commandboard-api credshare backend (superseded by the PWA) and its Supabase/Turso stores + libsql dep; commandboard-api tests green (40). - removed the Next.js /teams page (the PWA is the web UI now). Verified end-to-end: two accounts register on the PWA, mint lsk_ keys, CLI login uploads identity keys, owner pushes an encrypted .env, teammate invited -> accepted -> granted -> pulls the exact file. Server stores ciphertext only. Full workspace build + tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f057589d66
commit
9ba044577f
46 changed files with 2785 additions and 1730 deletions
81
apps/pwa/src/routes/auth.mjs
Normal file
81
apps/pwa/src/routes/auth.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Email/password auth + the sign-in page (which also hosts passkey + CoinPay buttons).
|
||||
import { Router } from "express";
|
||||
import { page, footer, esc } from "../lib/html.mjs";
|
||||
import { csrfInput, createSession, destroySession, takeNext } from "../lib/session.mjs";
|
||||
import { hashPassword, verifyPassword } from "../lib/crypto.mjs";
|
||||
import { createUserWithPassword, userByEmail } from "../lib/users.mjs";
|
||||
import { dashboardHandler } from "./pages.mjs";
|
||||
import { config } from "../config.mjs";
|
||||
|
||||
export const authRouter = Router();
|
||||
|
||||
function authPage(req, { error = "", mode = "in" } = {}) {
|
||||
const body = `
|
||||
<main class="wrap" style="max-width:440px;padding-top:8vh">
|
||||
<a class="brand" href="/" style="justify-content:center;font-size:1.5rem;margin-bottom:6px"><span class="mark">LS</span>LogicSRC<span class="app">credentials</span></a>
|
||||
<p class="label" style="text-align:center;margin-bottom:26px">Share secrets, end-to-end encrypted</p>
|
||||
<div class="card"><div class="card-body">
|
||||
${error ? `<div class="notice err">${esc(error)}</div>` : ""}
|
||||
<form method="post" action="/auth/${mode === "up" ? "register" : "login"}">
|
||||
${csrfInput(req)}
|
||||
<label class="field"><span>Email</span>
|
||||
<input type="email" name="email" autocomplete="username" required placeholder="you@example.com" value="${esc(req.query.email || "")}"></label>
|
||||
<label class="field"><span>Password</span>
|
||||
<input type="password" name="password" autocomplete="${mode === "up" ? "new-password" : "current-password"}" required minlength="8" placeholder="8+ characters"></label>
|
||||
<button class="btn acid block" type="submit">${mode === "up" ? "Create account" : "Sign in"}</button>
|
||||
</form>
|
||||
<p class="mono" style="text-align:center;font-size:.74rem;margin:14px 0 0">
|
||||
${mode === "up"
|
||||
? `Already have an account? <a class="acid" href="/?mode=in">Sign in</a>`
|
||||
: `New here? <a class="acid" href="/?mode=up">Create account</a>`}
|
||||
</p>
|
||||
|
||||
<div class="divider">or</div>
|
||||
|
||||
<button class="btn block" type="button" id="passkey-btn" style="margin-bottom:10px">🔑 Continue with a passkey</button>
|
||||
${config.coinpayLoginEnabled
|
||||
? `<a class="btn block" href="/auth/coinpay/start">◆ Continue with CoinPay</a>`
|
||||
: `<button class="btn block" type="button" disabled title="Set COINPAY_OAUTH_* to enable">◆ Continue with CoinPay</button>`}
|
||||
<p id="passkey-msg" class="mono faint" style="font-size:.72rem;text-align:center;margin:12px 0 0"></p>
|
||||
</div></div>
|
||||
</main>${footer}
|
||||
<script src="/vendor/simplewebauthn-browser.umd.js"></script>
|
||||
<script src="/passkey.js"></script>`;
|
||||
return page({ title: "LogicSRC ▸ sign in", body });
|
||||
}
|
||||
|
||||
authRouter.get("/", (req, res) => {
|
||||
if (req.user) {
|
||||
const next = takeNext(req, res);
|
||||
if (next) return res.redirect(next);
|
||||
return dashboardHandler(req, res); // dashboard lives at the root
|
||||
}
|
||||
res.type("html").send(authPage(req, { mode: req.query.mode === "up" ? "up" : "in" }));
|
||||
});
|
||||
|
||||
authRouter.post("/auth/register", async (req, res) => {
|
||||
const email = String(req.body.email || "").trim().toLowerCase();
|
||||
const password = String(req.body.password || "");
|
||||
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return res.type("html").send(authPage(req, { mode: "up", error: "Enter a valid email." }));
|
||||
if (password.length < 8) return res.type("html").send(authPage(req, { mode: "up", error: "Password must be at least 8 characters." }));
|
||||
if (await userByEmail(email)) return res.type("html").send(authPage(req, { mode: "up", error: "That email already has an account — sign in." }));
|
||||
const user = await createUserWithPassword(email, hashPassword(password));
|
||||
await createSession(res, user.id);
|
||||
res.redirect(takeNext(req, res) || "/");
|
||||
});
|
||||
|
||||
authRouter.post("/auth/login", async (req, res) => {
|
||||
const email = String(req.body.email || "").trim().toLowerCase();
|
||||
const password = String(req.body.password || "");
|
||||
const user = await userByEmail(email);
|
||||
if (!user || !verifyPassword(password, user.password_hash)) {
|
||||
return res.type("html").send(authPage(req, { mode: "in", error: "Wrong email or password." }));
|
||||
}
|
||||
await createSession(res, user.id);
|
||||
res.redirect(takeNext(req, res) || "/");
|
||||
});
|
||||
|
||||
authRouter.post("/auth/logout", async (req, res) => {
|
||||
await destroySession(req, res);
|
||||
res.redirect("/");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue