Revert NocoDB auto sign-in, keep CSP fix for embed proxy

NocoDB v2 stores auth tokens in-memory (Pinia store), not in cookies
accessible to external pages. The auth bridge approach can't inject
tokens into NocoDB's SPA state. Reverted to the original banner
approach ("sign in to NocoDB in a new tab").

Kept: CSP fix (frame-ancestors http://localhost:* instead of just
localhost, which only matched port 80).

Bunker Admin
This commit is contained in:
bunker-admin 2026-04-09 14:01:02 -06:00
parent aa69048024
commit 5f0ae6bc5a
6 changed files with 28 additions and 118 deletions

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import { Button, Space, Badge, Spin, Grid, Result, Alert } from 'antd'; import { Button, Space, Badge, Spin, Grid, Result, Alert } from 'antd';
import { ReloadOutlined, LinkOutlined, DatabaseOutlined } from '@ant-design/icons'; import { ReloadOutlined, LinkOutlined, DatabaseOutlined } from '@ant-design/icons';
@ -7,6 +7,8 @@ import type { AppOutletContext } from '@/components/AppLayout';
import type { ServicesStatus, ServicesConfig } from '@/types/api'; import type { ServicesStatus, ServicesConfig } from '@/types/api';
import { buildServiceUrl } from '@/lib/service-url'; import { buildServiceUrl } from '@/lib/service-url';
const BANNER_DISMISSED_KEY = 'nocodb-auth-banner-dismissed';
export default function NocoDBPage() { export default function NocoDBPage() {
const { setPageHeader } = useOutletContext<AppOutletContext>(); const { setPageHeader } = useOutletContext<AppOutletContext>();
const screens = Grid.useBreakpoint(); const screens = Grid.useBreakpoint();
@ -15,9 +17,9 @@ export default function NocoDBPage() {
const [online, setOnline] = useState<boolean | null>(null); const [online, setOnline] = useState<boolean | null>(null);
const [config, setConfig] = useState<ServicesConfig | null>(null); const [config, setConfig] = useState<ServicesConfig | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [iframeSrc, setIframeSrc] = useState<string | null>(null); const [bannerDismissed, setBannerDismissed] = useState(
const [authFailed, setAuthFailed] = useState(false); () => localStorage.getItem(BANNER_DISMISSED_KEY) === 'true'
const authAttempted = useRef(false); );
const fetchStatus = useCallback(async () => { const fetchStatus = useCallback(async () => {
try { try {
@ -42,31 +44,7 @@ export default function NocoDBPage() {
? buildServiceUrl(config.nocodbSubdomain, config.domain, config.nocodbPort) ? buildServiceUrl(config.nocodbSubdomain, config.domain, config.nocodbPort)
: null; : null;
// Auto sign-in: fetch NocoDB auth token and navigate iframe to auth bridge
useEffect(() => {
if (!serviceUrl || !online || authAttempted.current) return;
authAttempted.current = true;
(async () => {
try {
const res = await api.get<{ token: string }>('/services/nocodb-auth');
if (res.data.token) {
// Navigate iframe to auth bridge (same origin as NocoDB) which sets the cookie
setIframeSrc(`${serviceUrl}/auth-bridge#${res.data.token}`);
return;
}
} catch {
// Auth endpoint unavailable — fall back to direct URL
}
setAuthFailed(true);
setIframeSrc(serviceUrl);
})();
}, [serviceUrl, online]);
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
authAttempted.current = false;
setIframeSrc(null);
setAuthFailed(false);
fetchStatus(); fetchStatus();
}, [fetchStatus]); }, [fetchStatus]);
@ -137,11 +115,11 @@ export default function NocoDBPage() {
return ( return (
<div style={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}> <div style={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>
{authFailed && ( {!bannerDismissed && (
<Alert <Alert
message={ message={
<> <>
Auto sign-in unavailable. You may need to{' '} If the database browser appears blank, you may need to{' '}
<a href={serviceUrl} target="_blank" rel="noopener noreferrer"> <a href={serviceUrl} target="_blank" rel="noopener noreferrer">
sign in to NocoDB in a new tab sign in to NocoDB in a new tab
</a>{' '} </a>{' '}
@ -151,26 +129,23 @@ export default function NocoDBPage() {
type="info" type="info"
showIcon showIcon
closable closable
onClose={() => setAuthFailed(false)} onClose={() => {
setBannerDismissed(true);
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
}}
style={{ borderRadius: 0, flexShrink: 0 }} style={{ borderRadius: 0, flexShrink: 0 }}
/> />
)} )}
{iframeSrc ? ( <iframe
<iframe src={serviceUrl}
src={iframeSrc} style={{
style={{ width: '100%',
width: '100%', flex: 1,
flex: 1, border: 'none',
border: 'none', display: 'block',
display: 'block', }}
}} title="NocoDB"
title="NocoDB" />
/>
) : (
<div style={{ textAlign: 'center', padding: 80 }}>
<Spin size="large" tip="Signing in to NocoDB..." />
</div>
)}
</div> </div>
); );
} }

View File

@ -96,8 +96,6 @@ const envSchema = z.object({
// Platform Services (NocoDB, n8n, Gitea) // Platform Services (NocoDB, n8n, Gitea)
NOCODB_URL: z.string().default('http://changemaker-v2-nocodb:8080'), NOCODB_URL: z.string().default('http://changemaker-v2-nocodb:8080'),
NC_ADMIN_EMAIL: z.string().default(''),
NC_ADMIN_PASSWORD: z.string().default(''),
NOCODB_PORT: z.coerce.number().default(8091), NOCODB_PORT: z.coerce.number().default(8091),
NOCODB_EMBED_PORT: z.coerce.number().default(8881), NOCODB_EMBED_PORT: z.coerce.number().default(8881),
N8N_URL: z.string().default('http://n8n-changemaker:5678'), N8N_URL: z.string().default('http://n8n-changemaker:5678'),

View File

@ -64,48 +64,6 @@ router.get(
}, },
); );
// GET /api/services/nocodb-auth — proxy NocoDB signin to get an auth token for iframe auto-login
router.get(
'/nocodb-auth',
async (_req: Request, res: Response, next: NextFunction) => {
try {
if (!env.NC_ADMIN_EMAIL || !env.NC_ADMIN_PASSWORD) {
res.status(503).json({ error: 'NocoDB admin credentials not configured' });
return;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(`${env.NOCODB_URL}/api/v1/auth/user/signin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: env.NC_ADMIN_EMAIL, password: env.NC_ADMIN_PASSWORD }),
signal: controller.signal,
});
if (!response.ok) {
res.status(502).json({ error: 'NocoDB authentication failed' });
return;
}
const data = (await response.json()) as { token?: string };
if (!data.token) {
res.status(502).json({ error: 'No token in NocoDB response' });
return;
}
res.json({ token: data.token });
} finally {
clearTimeout(timeout);
}
} catch (err) {
logger.error('NocoDB auth proxy failed', err);
next(err);
}
},
);
// GET /api/services/config — return public-facing port numbers + subdomain info for iframe URLs // GET /api/services/config — return public-facing port numbers + subdomain info for iframe URLs
router.get( router.get(
'/config', '/config',

View File

@ -135,9 +135,6 @@ services:
- GITEA_DOCS_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite} - GITEA_DOCS_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite}
- GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs} - GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs}
- GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2} - GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2}
# NocoDB credentials (for auto sign-in proxy)
- NC_ADMIN_EMAIL=${NC_ADMIN_EMAIL:-admin@cmlite.org}
- NC_ADMIN_PASSWORD=${NC_ADMIN_PASSWORD:-}
# GeoIP (MaxMind GeoLite2) # GeoIP (MaxMind GeoLite2)
- MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-} - MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-}
- MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-} - MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-}

View File

@ -136,9 +136,6 @@ services:
- GITEA_DOCS_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite} - GITEA_DOCS_REPO=${GITEA_DOCS_REPO:-admin/changemaker.lite}
- GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs} - GITEA_DOCS_PREFIX=${GITEA_DOCS_PREFIX:-mkdocs/docs}
- GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2} - GITEA_DOCS_BRANCH=${GITEA_DOCS_BRANCH:-v2}
# NocoDB credentials (for auto sign-in proxy)
- NC_ADMIN_EMAIL=${NC_ADMIN_EMAIL:-admin@cmlite.org}
- NC_ADMIN_PASSWORD=${NC_ADMIN_PASSWORD:-}
# GeoIP (MaxMind GeoLite2) # GeoIP (MaxMind GeoLite2)
- MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-} - MAXMIND_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID:-}
- MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-} - MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY:-}

View File

@ -78,14 +78,6 @@ server {
server_name db.${DOMAIN}; server_name db.${DOMAIN};
add_header Content-Security-Policy "frame-ancestors 'self' app.${DOMAIN}" always; add_header Content-Security-Policy "frame-ancestors 'self' app.${DOMAIN}" always;
# Auth bridge for iframe auto-sign-in (token passed via URL hash, never sent to server)
location = /auth-bridge {
default_type text/html;
add_header Cache-Control "no-store" always;
add_header Content-Security-Policy "frame-ancestors 'self' app.${DOMAIN}" always;
return 200 '<!DOCTYPE html><html><head><meta charset="utf-8"><script>(function(){var t=location.hash.substring(1);if(!t){document.body.innerText="No token";return;}document.cookie="nocodb-token="+encodeURIComponent(t)+";path=/;SameSite=Lax;max-age=86400";try{localStorage.setItem("nocodb-token",JSON.stringify(t));}catch(e){}window.location.replace("/dashboard/");})()</script></head><body>Signing in...</body></html>';
}
location / { location / {
set $upstream_nocodb http://changemaker-v2-nocodb:8080; set $upstream_nocodb http://changemaker-v2-nocodb:8080;
proxy_pass $upstream_nocodb; proxy_pass $upstream_nocodb;
@ -296,19 +288,12 @@ server {
server { server {
listen ${NOCODB_EMBED_PORT}; listen ${NOCODB_EMBED_PORT};
# Auth bridge for iframe auto-sign-in (localhost/dev variant)
location = /auth-bridge {
default_type text/html;
add_header Cache-Control "no-store" always;
return 200 '<!DOCTYPE html><html><head><meta charset="utf-8"><script>(function(){var t=location.hash.substring(1);if(!t){document.body.innerText="No token";return;}document.cookie="nocodb-token="+encodeURIComponent(t)+";path=/;SameSite=Lax;max-age=86400";try{localStorage.setItem("nocodb-token",JSON.stringify(t));}catch(e){}window.location.replace("/dashboard/");})()</script></head><body>Signing in...</body></html>';
}
location / { location / {
set $upstream_nocodb http://changemaker-v2-nocodb:8080; set $upstream_nocodb http://changemaker-v2-nocodb:8080;
proxy_pass $upstream_nocodb; proxy_pass $upstream_nocodb;
proxy_hide_header X-Frame-Options; proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy; proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' localhost 127.0.0.1" always; add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:* http://127.0.0.1:*" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -356,7 +341,7 @@ server {
proxy_pass $upstream_gitea; proxy_pass $upstream_gitea;
proxy_hide_header X-Frame-Options; proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy; proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' localhost 127.0.0.1" always; add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:* http://127.0.0.1:*" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -389,7 +374,7 @@ server {
proxy_pass $upstream_miniqr; proxy_pass $upstream_miniqr;
proxy_hide_header X-Frame-Options; proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy; proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' localhost 127.0.0.1" always; add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:* http://127.0.0.1:*" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -603,7 +588,7 @@ server {
proxy_pass $upstream_homepage; proxy_pass $upstream_homepage;
proxy_hide_header X-Frame-Options; proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy; proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' localhost 127.0.0.1" always; add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:* http://127.0.0.1:*" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -656,7 +641,7 @@ server {
proxy_pass $upstream_gancio; proxy_pass $upstream_gancio;
proxy_hide_header X-Frame-Options; proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy; proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' localhost 127.0.0.1" always; add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:* http://127.0.0.1:*" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -707,7 +692,7 @@ server {
proxy_pass $upstream_alertmanager; proxy_pass $upstream_alertmanager;
proxy_hide_header X-Frame-Options; proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy; proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' localhost 127.0.0.1" always; add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:* http://127.0.0.1:*" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;