Add authenticated dashboard and route deployment through Kong

This commit is contained in:
2026-10-01 20:45:54 +00:00
parent 491f464242
commit 1dddc146ff
28 changed files with 798 additions and 166 deletions

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Generate stable local auth secrets. Nothing privileged is emitted to stdout."""
import base64
import hashlib
import hmac
import json
import os
from pathlib import Path
import secrets
import time
ROOT = Path(__file__).resolve().parents[1]
AUTH_DIR = ROOT / 'deploy/supabase'
def read_env(path):
return dict(line.split('=', 1) for line in path.read_text().splitlines() if line and not line.startswith('#'))
def jwt(secret, role):
def encode(value):
return base64.urlsafe_b64encode(json.dumps(value, separators=(',', ':')).encode()).rstrip(b'=').decode()
now = int(time.time())
message = encode({'alg': 'HS256', 'typ': 'JWT'}) + '.' + encode({'role': role, 'iss': 'supabase', 'iat': now, 'exp': now + 10 * 365 * 86400})
signature = base64.urlsafe_b64encode(hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()).rstrip(b'=').decode()
return message + '.' + signature
def write_private(path, content):
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, 'w') as output:
output.write(content)
path.chmod(0o600)
def main():
env_path = AUTH_DIR / '.env'
if not env_path.exists():
secret = secrets.token_hex(32)
demo_path = ROOT / 'DEMO_USER.txt'
demo = dict(line.split(': ', 1) for line in demo_path.read_text().splitlines() if ': ' in line) if demo_path.exists() else {}
values = {
'POSTGRES_PASSWORD': secrets.token_hex(24),
'JWT_SECRET': secret,
'SUPABASE_ANON_KEY': jwt(secret, 'anon'),
'SUPABASE_SERVICE_KEY': jwt(secret, 'service_role'),
'SUPABASE_PORT': os.getenv('SUPABASE_PORT', '54321'),
'KONG_BIND_ADDRESS': os.getenv('KONG_BIND_ADDRESS', '0.0.0.0'),
'SITE_URL': os.getenv('SITE_URL', 'https://defense.naori.cloud'),
'DEMO_EMAIL': demo.get('Username', 'demo@defense.naori.cloud'),
'DEMO_PASSWORD': demo.get('Password', secrets.token_urlsafe(18)),
}
write_private(env_path, ''.join(f'{key}={value}\n' for key, value in values.items()))
print('Generated auth secrets and demo credentials in deploy/supabase/.env')
values = read_env(env_path)
config = {
'_format_version': '2.1', '_transform': True,
'consumers': [
{'username': 'anon', 'keyauth_credentials': [{'key': values['SUPABASE_ANON_KEY']}]},
{'username': 'service_role', 'keyauth_credentials': [{'key': values['SUPABASE_SERVICE_KEY']}]},
],
'services': [
{'name': name, 'url': url, 'routes': [{'name': name, 'paths': [path], 'strip_path': True}],
'plugins': [{'name': 'cors'}, {'name': 'key-auth', 'config': {'hide_credentials': False}}]}
for name, url, path in [('auth', 'http://auth:9999/', '/auth/v1/'), ('rest', 'http://rest:3000/', '/rest/v1/')]
],
}
config['services'].extend([
{'name': 'simulation', 'url': 'http://host.docker.internal:8000',
'routes': [{'name': 'simulation', 'paths': ['/socket.io/'], 'strip_path': False}],
'read_timeout': 60000, 'write_timeout': 60000},
{'name': 'frontend', 'url': 'http://frontend:80',
'routes': [{'name': 'frontend', 'paths': ['/'], 'strip_path': False}]},
])
# Kong runs as UID 1337; its bind-mounted config must be readable inside the container.
write_private(AUTH_DIR / 'kong.json.local', json.dumps(config, indent=2))
(AUTH_DIR / 'kong.json.local').chmod(0o644)
write_private(ROOT / 'deploy/auth.env.local', f"SUPABASE_URL=http://127.0.0.1:{values['SUPABASE_PORT']}\nSUPABASE_ANON_KEY={values['SUPABASE_ANON_KEY']}\n")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Provision and verify the demo account without replacing an existing password."""
import json
import sys
import urllib.error
import urllib.request
from bootstrap_auth import AUTH_DIR, read_env
def request(url, key, method='GET', payload=None):
headers = {'apikey': key, 'Authorization': f'Bearer {key}', 'Content-Type': 'application/json'}
body = json.dumps(payload).encode() if payload is not None else None
with urllib.request.urlopen(urllib.request.Request(url, data=body, headers=headers, method=method), timeout=15) as response:
return json.load(response)
def main():
values = read_env(AUTH_DIR / '.env')
url = f"http://127.0.0.1:{values['SUPABASE_PORT']}/auth/v1"
credentials = {'email': values['DEMO_EMAIL'], 'password': values['DEMO_PASSWORD']}
existing = None
page = 1
while True:
users = request(f'{url}/admin/users?page={page}&per_page=100', values['SUPABASE_SERVICE_KEY']).get('users', [])
existing = next((user for user in users if user.get('email') == credentials['email']), None)
if existing or len(users) < 100:
break
page += 1
if not existing:
request(f'{url}/admin/users', values['SUPABASE_SERVICE_KEY'], 'POST', {
**credentials, 'email_confirm': True, 'user_metadata': {'name': 'Demo user'},
})
print('Created demo account.')
session = request(f'{url}/token?grant_type=password', values['SUPABASE_ANON_KEY'], 'POST', credentials)
if not session.get('access_token') or not session.get('user', {}).get('email') == credentials['email']:
raise RuntimeError('Demo login verification failed')
print('Demo password login verified. Credentials are stored in deploy/supabase/.env.')
if __name__ == '__main__':
try:
main()
except urllib.error.HTTPError as error:
print(f'Demo account setup failed (HTTP {error.code}).', file=sys.stderr)
sys.exit(1)