Files
gashboard/apps/web/src/stores/auth.ts
Dorian c56a47e2c4 feat(web): cyberpunk vue 3 dashboard with primal/amber/extension login
Frontend in @gashboard/web (Vue 3 + Vite + Pinia + TS, ~1.2k LoC):

Auth flow:
  - Two signing paths: NIP-07 browser extension (Alby/nos2x/Primal
    extension via window.nostr) and NIP-46 remote signer (Primal
    app, Amber, nsecbunker via bunker:// URI).
  - applesauce-signers lazy-loaded only on bunker login so users
    with NIP-07 don't pay the cost.
  - NIP-98 event built client-side, posted to /api/auth/login,
    JWT persisted in localStorage. Pinia auth store handles
    login/logout/state restore on reload.

Dashboard (composes the live /api/datum/stats poll, 5s):
  - PoolHero — combined hashrate as the headline number,
    block height, subscribed count, accepted/rejected shares.
  - LotteryWidget — rotating self-deprecating odds copy
    ("you're 0.3× as likely to find a block as get hit by
    lightning today"). Uses ~720 EH/s as the network-hashrate
    constant (TODO: fetch live).
  - ShareTicker — SVG sparkline of the last 60 polls.
  - MinerCard ×N — nickname (QU4CK/P1XEL/N4N0/M1N1), live
    hashrate, last share, lifetime tickets, reject %, status
    glow (green hashing / amber stale / red idle), affectionate
    roast subtitle per ASIC type.
  - BlockCelebration — full-screen overlay with celebration
    copy. Dormant for now (Datum's lastBlockFoundAt isn't
    surfaced yet); preview via window.gashboardCelebrate().

Cyberpunk theme:
  - Pure CSS vars, no Tailwind. Dark bg, neon cyan/magenta
    accents, monospace, glow shadows.
  - Optional CRT scanlines toggle (persists to localStorage).
  - Mobile-aware grid breakpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:59:00 +01:00

61 lines
1.6 KiB
TypeScript

import { defineStore } from "pinia";
import { computed, ref } from "vue";
import * as api from "../services/api";
import * as signer from "../services/signer";
export const useAuthStore = defineStore("auth", () => {
const stored = api.loadToken();
const npub = ref<string | null>(stored?.npub ?? null);
const token = ref<string | null>(stored?.token ?? null);
const error = ref<string | null>(null);
const busy = ref(false);
const isLoggedIn = computed(() => !!token.value);
async function loginExtension(): Promise<void> {
error.value = null;
busy.value = true;
try {
await signer.loginWithExtension();
const r = await api.login();
token.value = r.token;
npub.value = r.npub;
api.saveToken(r.token, r.npub);
} catch (e) {
signer.clearSigner();
error.value = e instanceof Error ? e.message : "Login failed";
throw e;
} finally {
busy.value = false;
}
}
async function loginBunker(uri: string): Promise<void> {
error.value = null;
busy.value = true;
try {
await signer.loginWithBunker(uri);
const r = await api.login();
token.value = r.token;
npub.value = r.npub;
api.saveToken(r.token, r.npub);
} catch (e) {
signer.clearSigner();
error.value = e instanceof Error ? e.message : "Login failed";
throw e;
} finally {
busy.value = false;
}
}
function logout(): void {
api.clearToken();
signer.clearSigner();
token.value = null;
npub.value = null;
error.value = null;
}
return { npub, token, error, busy, isLoggedIn, loginExtension, loginBunker, logout };
});