Replace Astro hub build with standalone hub/index.html
This commit is contained in:
+279
@@ -0,0 +1,279 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it" style="background:#09090b">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Bincio</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var palettes = {
|
||||||
|
default: { accent: '#60a5fa', dim: 'rgba(96,165,250,0.15)' },
|
||||||
|
giro: { accent: '#f472b6', dim: 'rgba(244,114,182,0.15)' },
|
||||||
|
tour: { accent: '#facc15', dim: 'rgba(250,204,21,0.15)' },
|
||||||
|
vuelta: { accent: '#ef4444', dim: 'rgba(239,68,68,0.15)' },
|
||||||
|
};
|
||||||
|
var races = [
|
||||||
|
{ key: 'giro', start: [4, 8], end: [5, 1] },
|
||||||
|
{ key: 'tour', start: [5, 27], end: [6, 19] },
|
||||||
|
{ key: 'vuelta', start: [7, 15], end: [8, 6] },
|
||||||
|
];
|
||||||
|
function autoKey() {
|
||||||
|
var now = new Date(), y = now.getFullYear();
|
||||||
|
for (var i = 0; i < races.length; i++) {
|
||||||
|
var r = races[i];
|
||||||
|
if (now >= new Date(y, r.start[0], r.start[1]) &&
|
||||||
|
now < new Date(y, r.end[0], r.end[1] + 1)) return r.key;
|
||||||
|
}
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
var p = palettes[autoKey()];
|
||||||
|
document.documentElement.style.setProperty('--accent', p.accent);
|
||||||
|
document.documentElement.style.setProperty('--accent-dim', p.dim);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
:root {
|
||||||
|
--accent: #60a5fa;
|
||||||
|
--accent-dim: rgba(96,165,250,0.15);
|
||||||
|
--bg: #09090b;
|
||||||
|
--bg-card: #18181b;
|
||||||
|
--border: #27272a;
|
||||||
|
--border-hover: #52525b;
|
||||||
|
--text: #ffffff;
|
||||||
|
--text-muted: #71717a;
|
||||||
|
--text-dim: #3f3f46;
|
||||||
|
--red: #f87171;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
body { visibility: hidden; }
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
max-width: 384px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 4rem 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
input[type=text], input[type=password] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
input[type=text]:focus, input[type=password]:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.field { margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { opacity: 0.85; }
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--red);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.forgot a {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.forgot a:hover { color: var(--text-dim); }
|
||||||
|
|
||||||
|
/* App cards */
|
||||||
|
.greeting {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.cards { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||||
|
.card {
|
||||||
|
display: block;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.card:hover { border-color: var(--border-hover); }
|
||||||
|
.card-title { font-weight: 600; color: var(--text); }
|
||||||
|
.card-sub { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.125rem; }
|
||||||
|
|
||||||
|
.logout-wrap { text-align: center; margin-top: 2rem; }
|
||||||
|
.btn-logout {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-logout:hover { color: var(--text-muted); }
|
||||||
|
|
||||||
|
#hub-apps { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<div id="hub-login">
|
||||||
|
<p class="tagline">mangia<br>bevi<br>stai calmo<br>non strappare</p>
|
||||||
|
<h1>Bincio</h1>
|
||||||
|
<form id="login-form">
|
||||||
|
<div class="field">
|
||||||
|
<label for="handle">Handle</label>
|
||||||
|
<input id="handle" name="handle" type="text" autocomplete="username" placeholder="your handle" required>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||||||
|
</div>
|
||||||
|
<p id="login-error" class="error"></p>
|
||||||
|
<button type="submit" class="btn-primary">Sign in</button>
|
||||||
|
</form>
|
||||||
|
<p class="forgot">
|
||||||
|
<a href="https://activity.bincio.org/reset-password/">Forgot password?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="hub-apps">
|
||||||
|
<p id="hub-greeting" class="greeting"></p>
|
||||||
|
<p class="tagline">mangia<br>bevi<br>stai calmo<br>non strappare</p>
|
||||||
|
<div id="hub-cards" class="cards"></div>
|
||||||
|
<div class="logout-wrap">
|
||||||
|
<button id="hub-logout" class="btn-logout">Log out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ACTIVITY_URL = 'https://activity.bincio.org';
|
||||||
|
const WIKI_URL = 'https://wiki.bincio.org';
|
||||||
|
|
||||||
|
const loginDiv = document.getElementById('hub-login');
|
||||||
|
const appsDiv = document.getElementById('hub-apps');
|
||||||
|
const greeting = document.getElementById('hub-greeting');
|
||||||
|
const cardsDiv = document.getElementById('hub-cards');
|
||||||
|
const form = document.getElementById('login-form');
|
||||||
|
const errEl = document.getElementById('login-error');
|
||||||
|
|
||||||
|
function appCard(title, sub, href) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.className = 'card';
|
||||||
|
a.href = href;
|
||||||
|
a.innerHTML = `<p class="card-title">${title}</p><p class="card-sub">${sub}</p>`;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showApps(user) {
|
||||||
|
loginDiv.style.display = 'none';
|
||||||
|
appsDiv.style.display = '';
|
||||||
|
greeting.textContent = 'Ciao ' + (user.display_name || user.handle);
|
||||||
|
cardsDiv.innerHTML = '';
|
||||||
|
if (user.activity_access)
|
||||||
|
cardsDiv.appendChild(appCard('BincioActivity', 'Tracks, strade e numeri', ACTIVITY_URL));
|
||||||
|
if (user.wiki_access)
|
||||||
|
cardsDiv.appendChild(appCard('BincioWiki', 'La memoria collettiva del gruppo', WIKI_URL));
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLogin() {
|
||||||
|
appsDiv.style.display = 'none';
|
||||||
|
loginDiv.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/me', { credentials: 'include' })
|
||||||
|
.then(async r => {
|
||||||
|
document.body.style.visibility = '';
|
||||||
|
if (r.ok) showApps(await r.json());
|
||||||
|
})
|
||||||
|
.catch(() => { document.body.style.visibility = ''; });
|
||||||
|
|
||||||
|
form.addEventListener('submit', async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
errEl.style.display = 'none';
|
||||||
|
const handle = form.querySelector('#handle').value.trim();
|
||||||
|
const password = form.querySelector('#password').value;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ handle, password }),
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const d = await r.json().catch(() => ({}));
|
||||||
|
errEl.textContent = d.detail ?? 'Invalid credentials';
|
||||||
|
errEl.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showApps(await r.json());
|
||||||
|
} catch {
|
||||||
|
errEl.textContent = 'Could not reach server';
|
||||||
|
errEl.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('hub-logout').addEventListener('click', async () => {
|
||||||
|
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch (_) {}
|
||||||
|
form.querySelector('#handle').value = '';
|
||||||
|
form.querySelector('#password').value = '';
|
||||||
|
showLogin();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+2
-6
@@ -145,7 +145,7 @@ def prepare_serve() -> None:
|
|||||||
|
|
||||||
# ── 4. Hand off to bincio dev ─────────────────────────────────────────────────
|
# ── 4. Hand off to bincio dev ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def start_dev(mobile: bool = False, hub: bool = False) -> None:
|
def start_dev(mobile: bool = False) -> None:
|
||||||
section("Starting bincio dev")
|
section("Starting bincio dev")
|
||||||
print()
|
print()
|
||||||
print(" \033[1mCredentials\033[0m")
|
print(" \033[1mCredentials\033[0m")
|
||||||
@@ -163,9 +163,6 @@ def start_dev(mobile: bool = False, hub: bool = False) -> None:
|
|||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.setdefault("PUBLIC_WIKI_URL", os.environ.get("WIKI_DEV_URL", "http://localhost:4322"))
|
env.setdefault("PUBLIC_WIKI_URL", os.environ.get("WIKI_DEV_URL", "http://localhost:4322"))
|
||||||
# --hub: simulate bincio.org hub mode (/ becomes login + app selector)
|
|
||||||
if hub:
|
|
||||||
env.setdefault("PUBLIC_ACTIVITY_URL", "http://localhost:4321")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(cmd, cwd=PROJECT_DIR, env=env)
|
subprocess.run(cmd, cwd=PROJECT_DIR, env=env)
|
||||||
@@ -193,7 +190,6 @@ def main() -> None:
|
|||||||
parser.add_argument("--fresh", action="store_true", help="Wipe DATA_DIR before starting")
|
parser.add_argument("--fresh", action="store_true", help="Wipe DATA_DIR before starting")
|
||||||
parser.add_argument("--no-dev", action="store_true", help="Stop after extract, skip bincio dev")
|
parser.add_argument("--no-dev", action="store_true", help="Stop after extract, skip bincio dev")
|
||||||
parser.add_argument("--mobile", action="store_true", help="Bind API to 0.0.0.0 for local mobile testing")
|
parser.add_argument("--mobile", action="store_true", help="Bind API to 0.0.0.0 for local mobile testing")
|
||||||
parser.add_argument("--hub", action="store_true", help="Simulate hub mode: / becomes login+app selector")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
raise_open_file_limit()
|
raise_open_file_limit()
|
||||||
@@ -211,7 +207,7 @@ def main() -> None:
|
|||||||
prepare_serve()
|
prepare_serve()
|
||||||
|
|
||||||
if not args.no_dev:
|
if not args.no_dev:
|
||||||
start_dev(mobile=args.mobile, hub=args.hub)
|
start_dev(mobile=args.mobile)
|
||||||
else:
|
else:
|
||||||
print(f"\n\033[32mDone.\033[0m Data ready at {DATA_DIR}")
|
print(f"\n\033[32mDone.\033[0m Data ready at {DATA_DIR}")
|
||||||
print(f"Run: uv run bincio dev --data-dir {DATA_DIR}\n")
|
print(f"Run: uv run bincio dev --data-dir {DATA_DIR}\n")
|
||||||
|
|||||||
+1
-150
@@ -4,71 +4,12 @@ import ActivityFeed from '../components/ActivityFeed.svelte';
|
|||||||
import { readShardHandles, isInstancePrivate } from '../lib/manifest';
|
import { readShardHandles, isInstancePrivate } from '../lib/manifest';
|
||||||
|
|
||||||
const base = import.meta.env.BASE_URL;
|
const base = import.meta.env.BASE_URL;
|
||||||
const activityUrl = import.meta.env.PUBLIC_ACTIVITY_URL ?? '';
|
|
||||||
const wikiUrl = import.meta.env.PUBLIC_WIKI_URL ?? '';
|
|
||||||
|
|
||||||
const shards = readShardHandles();
|
const shards = readShardHandles();
|
||||||
const isSingleUser = shards.length === 1 && !isInstancePrivate();
|
const isSingleUser = shards.length === 1 && !isInstancePrivate();
|
||||||
const singleHandle = isSingleUser ? shards[0].handle : null;
|
const singleHandle = isSingleUser ? shards[0].handle : null;
|
||||||
---
|
---
|
||||||
|
|
||||||
{activityUrl ? (
|
{isSingleUser ? (
|
||||||
<!-- Hub mode: bincio.org - login form / app selector -->
|
|
||||||
<Base title="Bincio" public={true}>
|
|
||||||
<div class="max-w-sm mx-auto mt-16 px-4"
|
|
||||||
id="hub-root"
|
|
||||||
data-activity-url={activityUrl}
|
|
||||||
data-wiki-url={wikiUrl}
|
|
||||||
style="visibility:hidden">
|
|
||||||
|
|
||||||
<!-- Login (shown when not authenticated) -->
|
|
||||||
<div id="hub-login">
|
|
||||||
<p class="text-center text-zinc-600 text-sm italic mb-8 leading-relaxed">
|
|
||||||
mangia<br/>bevi<br/>stai calmo<br/>non strappare
|
|
||||||
</p>
|
|
||||||
<h1 class="text-2xl font-bold text-white mb-6 text-center">Bincio</h1>
|
|
||||||
<form id="login-form" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-zinc-400 mb-1" for="handle">Handle</label>
|
|
||||||
<input id="handle" name="handle" type="text" autocomplete="username"
|
|
||||||
class="w-full px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-700 text-white placeholder-zinc-500 focus:outline-none focus:border-[--accent]"
|
|
||||||
placeholder="your handle" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-zinc-400 mb-1" for="password">Password</label>
|
|
||||||
<input id="password" name="password" type="password" autocomplete="current-password"
|
|
||||||
class="w-full px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-700 text-white focus:outline-none focus:border-[--accent]"
|
|
||||||
required />
|
|
||||||
</div>
|
|
||||||
<p id="login-error" class="text-red-400 text-sm hidden"></p>
|
|
||||||
<button type="submit"
|
|
||||||
class="w-full py-2 rounded-lg bg-[--accent] hover:opacity-90 text-white font-medium transition-opacity">
|
|
||||||
Sign in
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<p class="text-center text-zinc-500 text-sm mt-6">
|
|
||||||
<a href="/reset-password/" class="hover:text-zinc-400 transition-colors">Forgot password?</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- App selector (shown when authenticated) -->
|
|
||||||
<div id="hub-apps" style="display:none">
|
|
||||||
<p id="hub-greeting" class="text-center text-zinc-400 text-sm mb-4"></p>
|
|
||||||
<p class="text-center text-zinc-600 text-sm italic mb-8 leading-relaxed">
|
|
||||||
mangia<br/>bevi<br/>stai calmo<br/>non strappare
|
|
||||||
</p>
|
|
||||||
<div id="hub-cards" class="space-y-3"></div>
|
|
||||||
<p class="text-center mt-8">
|
|
||||||
<button id="hub-logout"
|
|
||||||
class="text-xs text-zinc-600 hover:text-zinc-400 transition-colors">
|
|
||||||
Log out
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Base>
|
|
||||||
) : isSingleUser ? (
|
|
||||||
<!-- Single-user: redirect / -> /u/{handle}/ -->
|
<!-- Single-user: redirect / -> /u/{handle}/ -->
|
||||||
<meta http-equiv="refresh" content={`0;url=${base}u/${singleHandle}/`} />
|
<meta http-equiv="refresh" content={`0;url=${base}u/${singleHandle}/`} />
|
||||||
<script define:vars={{ base, singleHandle }}>
|
<script define:vars={{ base, singleHandle }}>
|
||||||
@@ -81,93 +22,3 @@ const singleHandle = isSingleUser ? shards[0].handle : null;
|
|||||||
<ActivityFeed {base} client:only="svelte" />
|
<ActivityFeed {base} client:only="svelte" />
|
||||||
</Base>
|
</Base>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<script>
|
|
||||||
const root = document.getElementById('hub-root') as HTMLElement | null;
|
|
||||||
if (!root) {
|
|
||||||
// Not in hub mode - nothing to do.
|
|
||||||
} else {
|
|
||||||
const activityUrl = root.dataset.activityUrl ?? '';
|
|
||||||
const wikiUrl = root.dataset.wikiUrl ?? '';
|
|
||||||
|
|
||||||
const loginDiv = document.getElementById('hub-login') as HTMLElement;
|
|
||||||
const appsDiv = document.getElementById('hub-apps') as HTMLElement;
|
|
||||||
const greeting = document.getElementById('hub-greeting') as HTMLElement;
|
|
||||||
const cardsDiv = document.getElementById('hub-cards') as HTMLElement;
|
|
||||||
const form = document.getElementById('login-form') as HTMLFormElement;
|
|
||||||
const errEl = document.getElementById('login-error') as HTMLElement;
|
|
||||||
|
|
||||||
function appCard(label: string, sub: string, href: string): HTMLAnchorElement {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = href;
|
|
||||||
a.className = 'block rounded-xl bg-zinc-900 border border-zinc-800 px-5 py-4 hover:border-zinc-600 transition-colors';
|
|
||||||
a.innerHTML = `
|
|
||||||
<p class="font-semibold text-white">${label}</p>
|
|
||||||
<p class="text-xs text-zinc-500 mt-0.5">${sub}</p>
|
|
||||||
`;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showApps(user: { handle: string; display_name: string; activity_access: boolean; wiki_access: boolean }) {
|
|
||||||
loginDiv.style.display = 'none';
|
|
||||||
appsDiv.style.display = '';
|
|
||||||
greeting.textContent = `Ciao ${user.display_name || user.handle}`;
|
|
||||||
cardsDiv.innerHTML = '';
|
|
||||||
if (user.activity_access && activityUrl) {
|
|
||||||
// If activityUrl is the same origin (dev --hub mode), go to the user page
|
|
||||||
// directly so we don't loop back to the hub root.
|
|
||||||
const activityHref = activityUrl === window.location.origin
|
|
||||||
? `${activityUrl}/u/${user.handle}/`
|
|
||||||
: activityUrl;
|
|
||||||
cardsDiv.appendChild(appCard('BincioActivity', 'Tracks, strade e numeri', activityHref));
|
|
||||||
}
|
|
||||||
if (user.wiki_access && wikiUrl)
|
|
||||||
cardsDiv.appendChild(appCard('BincioWiki', 'La memoria collettiva del gruppo', wikiUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already authenticated
|
|
||||||
fetch('/api/me', { credentials: 'include' })
|
|
||||||
.then(async r => {
|
|
||||||
root.style.visibility = 'visible';
|
|
||||||
if (r.ok) showApps(await r.json());
|
|
||||||
// else: show login form (already visible)
|
|
||||||
})
|
|
||||||
.catch(() => { root.style.visibility = 'visible'; });
|
|
||||||
|
|
||||||
// Login form submit
|
|
||||||
form?.addEventListener('submit', async e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const handle = (form.querySelector('#handle') as HTMLInputElement).value.trim();
|
|
||||||
const password = (form.querySelector('#password') as HTMLInputElement).value;
|
|
||||||
errEl.classList.add('hidden');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch('/api/auth/login', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ handle, password }),
|
|
||||||
});
|
|
||||||
if (!r.ok) {
|
|
||||||
const d = await r.json().catch(() => ({}));
|
|
||||||
errEl.textContent = d.detail ?? 'Invalid credentials';
|
|
||||||
errEl.classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showApps(await r.json());
|
|
||||||
} catch {
|
|
||||||
errEl.textContent = 'Could not reach server';
|
|
||||||
errEl.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout
|
|
||||||
document.getElementById('hub-logout')?.addEventListener('click', async () => {
|
|
||||||
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch (_) {}
|
|
||||||
appsDiv.style.display = 'none';
|
|
||||||
loginDiv.style.display = '';
|
|
||||||
(form.querySelector('#handle') as HTMLInputElement).value = '';
|
|
||||||
(form.querySelector('#password') as HTMLInputElement).value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user