From 3b091653f26a8a3d33184a9cea0669115b249f90 Mon Sep 17 00:00:00 2001
From: Marco Allegretti
Date: Thu, 29 Jan 2026 11:38:43 +0100
Subject: [PATCH] demo: add brochure gated mode
---
compose/demo.yml | 1 +
frontend/src/layouts/Layout.astro | 10 +++-
frontend/src/layouts/PublicLayout.astro | 10 +++-
frontend/src/middleware.ts | 65 +++++++++++++++++++++++++
frontend/src/pages/communities.astro | 1 +
frontend/src/pages/demo.astro | 39 ++++++---------
frontend/src/pages/login.astro | 20 ++++++--
frontend/src/pages/proposals.astro | 1 +
frontend/src/pages/register.astro | 1 +
9 files changed, 118 insertions(+), 30 deletions(-)
create mode 100644 frontend/src/middleware.ts
diff --git a/compose/demo.yml b/compose/demo.yml
index 2955c1c..bd036b4 100644
--- a/compose/demo.yml
+++ b/compose/demo.yml
@@ -57,6 +57,7 @@ services:
environment:
INTERNAL_API_BASE: http://backend:3000
API_BASE: ${API_BASE:-http://localhost:3001}
+ PUBLIC_DEMO_SITE: ${PUBLIC_DEMO_SITE:-false}
depends_on:
- backend
diff --git a/frontend/src/layouts/Layout.astro b/frontend/src/layouts/Layout.astro
index 9630c5a..839bcdd 100644
--- a/frontend/src/layouts/Layout.astro
+++ b/frontend/src/layouts/Layout.astro
@@ -7,8 +7,16 @@ import { DEFAULT_THEME, themes as themeRegistry } from '../lib/themes';
import { API_BASE as apiBase } from '../lib/api';
import VotingIcons from '../components/icons/VotingIcons.astro';
+function isEnabled(v: string | undefined): boolean {
+ if (!v) return false;
+ const n = v.trim().toLowerCase();
+ return n === '1' || n === 'true' || n === 'yes' || n === 'on';
+}
+
const { title } = Astro.props;
+const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
+
const themes = Object.fromEntries(
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
);
@@ -84,7 +92,7 @@ const { title } = Astro.props;
diff --git a/frontend/src/layouts/PublicLayout.astro b/frontend/src/layouts/PublicLayout.astro
index de1f65a..e1b1404 100644
--- a/frontend/src/layouts/PublicLayout.astro
+++ b/frontend/src/layouts/PublicLayout.astro
@@ -6,8 +6,16 @@ interface Props {
import { DEFAULT_THEME, themes as themeRegistry } from '../lib/themes';
+function isEnabled(v: string | undefined): boolean {
+ if (!v) return false;
+ const n = v.trim().toLowerCase();
+ return n === '1' || n === 'true' || n === 'yes' || n === 'on';
+}
+
const { title, description = "Likwid is a modular governance engine for distributed organizations, open source communities, and civic movements." } = Astro.props;
+const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
+
const themes = Object.fromEntries(
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
);
@@ -83,7 +91,7 @@ const defaultTheme = DEFAULT_THEME;
diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts
new file mode 100644
index 0000000..aa03b18
--- /dev/null
+++ b/frontend/src/middleware.ts
@@ -0,0 +1,65 @@
+import { defineMiddleware } from 'astro:middleware';
+
+function isEnabled(v: string | undefined): boolean {
+ if (!v) return false;
+ const n = v.trim().toLowerCase();
+ return n === '1' || n === 'true' || n === 'yes' || n === 'on';
+}
+
+const DEMO_COOKIE = 'likwid_demo_unlocked';
+
+export const onRequest = defineMiddleware(async (context, next) => {
+ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
+ if (!publicDemoSite) {
+ return next();
+ }
+
+ const url = new URL(context.request.url);
+ const path = url.pathname;
+
+ if (path === '/demo' && url.searchParams.get('enter') === '1') {
+ let nextPath = url.searchParams.get('next') || '/communities';
+ if (!nextPath.startsWith('/')) {
+ nextPath = '/communities';
+ }
+
+ context.cookies.set(DEMO_COOKIE, '1', {
+ path: '/',
+ httpOnly: true,
+ sameSite: 'lax',
+ maxAge: 60 * 60 * 12,
+ });
+
+ return context.redirect(nextPath);
+ }
+
+ if (path.startsWith('/_astro') || path.startsWith('/favicon') || path.startsWith('/robots')) {
+ return next();
+ }
+
+ if (path === '/setup' || path === '/register') {
+ return context.redirect('/demo');
+ }
+
+ const isProtected =
+ path === '/login' ||
+ path === '/dashboard' ||
+ path === '/delegations' ||
+ path === '/notifications' ||
+ path === '/settings' ||
+ path.startsWith('/admin') ||
+ path.startsWith('/communities') ||
+ path.startsWith('/proposals') ||
+ path.startsWith('/users');
+
+ if (!isProtected) {
+ return next();
+ }
+
+ const unlocked = context.cookies.get(DEMO_COOKIE)?.value === '1';
+ if (!unlocked) {
+ return context.redirect('/demo');
+ }
+
+ return next();
+});
diff --git a/frontend/src/pages/communities.astro b/frontend/src/pages/communities.astro
index d9220a9..6c21d97 100644
--- a/frontend/src/pages/communities.astro
+++ b/frontend/src/pages/communities.astro
@@ -1,4 +1,5 @@
---
+export const prerender = false;
import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api';
---
diff --git a/frontend/src/pages/demo.astro b/frontend/src/pages/demo.astro
index 6b3a326..96f1d52 100644
--- a/frontend/src/pages/demo.astro
+++ b/frontend/src/pages/demo.astro
@@ -1,4 +1,5 @@
---
+export const prerender = false;
import PublicLayout from '../layouts/PublicLayout.astro';
import { API_BASE } from '../lib/api';
---
@@ -55,7 +56,7 @@ import { API_BASE } from '../lib/api';
Loading...
- Explore Aurora →
+ Explore Aurora →
@@ -103,7 +104,7 @@ import { API_BASE } from '../lib/api';
Navigate communities, read proposals, view voting results, and explore
delegation networks without any account. Full read access is public.
- Browse Communities →
+ Browse Communities →
@@ -199,8 +200,8 @@ import { API_BASE } from '../lib/api';
Use demo accounts to participate.
@@ -787,7 +788,7 @@ import { API_BASE } from '../lib/api';
}
// Demo login handler
- async function handleDemoLogin(username) {
+ async function loginDemoUser(username) {
const btn = document.querySelector('[data-username="' + username + '"]');
if (btn) {
btn.classList.add('loading');
@@ -810,22 +811,10 @@ import { API_BASE } from '../lib/api';
throw new Error('Login failed');
}
- const data = await response.json();
-
- // Store token in localStorage (use 'token' to match rest of app)
- localStorage.setItem('token', data.token);
- localStorage.setItem('user', JSON.stringify(data.user));
-
- // Update button to show success
- if (btn) {
- btn.querySelector('.btn-info span').textContent = 'Success! Redirecting...';
- }
-
- // Redirect to dashboard or communities
- setTimeout(() => {
- window.location.href = '/communities';
- }, 500);
-
+ const result = await response.json();
+ localStorage.setItem('token', result.token);
+ localStorage.setItem('user', JSON.stringify(result.user));
+ window.location.href = '/demo?enter=1&next=/communities';
} catch (error) {
console.error('Demo login failed:', error);
if (btn) {
@@ -841,6 +830,8 @@ import { API_BASE } from '../lib/api';
btn.querySelector('.btn-info span').textContent = texts[username] || '';
}, 2000);
}
+
+ alert('Demo login failed. Please try again.');
}
}
@@ -852,7 +843,7 @@ import { API_BASE } from '../lib/api';
btn.addEventListener('click', function() {
var username = btn.dataset.username;
if (username) {
- handleDemoLogin(username);
+ loginDemoUser(username);
}
});
})(buttons[i]);
diff --git a/frontend/src/pages/login.astro b/frontend/src/pages/login.astro
index fd52219..adeb555 100644
--- a/frontend/src/pages/login.astro
+++ b/frontend/src/pages/login.astro
@@ -1,6 +1,15 @@
---
+export const prerender = false;
import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api';
+
+function isEnabled(v: string | undefined): boolean {
+ if (!v) return false;
+ const n = v.trim().toLowerCase();
+ return n === '1' || n === 'true' || n === 'yes' || n === 'on';
+}
+
+const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
---
@@ -19,9 +28,11 @@ import { API_BASE as apiBase } from '../lib/api';
Login
-
- Don't have an account? Register
-
+ {!publicDemoSite ? (
+
+ Don't have an account? Register
+
+ ) : null}
@@ -56,7 +67,8 @@ import { API_BASE as apiBase } from '../lib/api';
const result = await res.json();
localStorage.setItem('token', result.token);
localStorage.setItem('user', JSON.stringify(result.user));
- window.location.href = '/';
+ const next = new URLSearchParams(window.location.search).get('next') || '/communities';
+ window.location.href = next;
} catch (err) {
errorEl.textContent = 'Connection error. Is the backend running?';
}
diff --git a/frontend/src/pages/proposals.astro b/frontend/src/pages/proposals.astro
index 9ef33e3..4dc32ee 100644
--- a/frontend/src/pages/proposals.astro
+++ b/frontend/src/pages/proposals.astro
@@ -1,4 +1,5 @@
---
+export const prerender = false;
import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api';
---
diff --git a/frontend/src/pages/register.astro b/frontend/src/pages/register.astro
index bd85763..8857bfa 100644
--- a/frontend/src/pages/register.astro
+++ b/frontend/src/pages/register.astro
@@ -1,4 +1,5 @@
---
+export const prerender = false;
import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api';
---