mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 06:47:28 +00:00
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>
81 lines
4.3 KiB
JavaScript
81 lines
4.3 KiB
JavaScript
// 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("/");
|
|
});
|