pre release changes

This commit is contained in:
seniorswe
2025-09-22 20:53:33 -04:00
committed by seniorswe
parent c7c82426ef
commit 3eae1cfd2c
9 changed files with 187 additions and 34 deletions
+31
View File
@@ -16,3 +16,34 @@ data/
# Local-only demo seeders (do not commit)
backend-services/scripts/seed_demo_data.py
# User-created artifacts and working files
# Cookie jars from curl or scripts
cookies.txt
**/cookies.txt
*cookies*.txt
# Ignore uploaded protos and generated gRPC code
backend-services/proto/
# Ensure nested generated directories are ignored (backend-services/generated, etc.)
**/generated/
# Ensure nested logs/certs/venv/data are ignored anywhere in repo
**/logs/
**/certs/
**/venv/
**/data/
# PIDs and OS/editor noise
*.pid
**/*.pid
.DS_Store
**/.DS_Store
# Local environment files (but keep examples tracked)
.env
**/.env
.env.local
**/.env.local
.env.development
.env.production
+5 -5
View File
@@ -33,7 +33,7 @@ Doorman can validate request payloads at the endpoint level before proxying to y
Create a validation schema
```bash
curl -X POST -b cookies.txt \
curl -X POST -b /tmp/doorman_cookies.txt \
-H 'Content-Type: application/json' \
http://localhost:5001/platform/endpoint/endpoint/validation \
-d '{
@@ -167,8 +167,8 @@ Defaults
Smoke checks
- Liveness: `curl -s http://localhost:5001/platform/monitor/liveness``{ "status": "alive" }`
- Readiness: `curl -s http://localhost:5001/platform/monitor/readiness``{ status: "ready", ... }`
- Auth login: `curl -s -c cookies.txt -H 'Content-Type: application/json' -d '{"email":"admin@localhost","password":"password1"}' http://localhost:5001/platform/authorization`
- Auth status: `curl -s -b cookies.txt http://localhost:5001/platform/authorization/status`
- Auth login: `curl -s -c /tmp/doorman_cookies.txt -H 'Content-Type: application/json' -d '{\"email\":\"admin@localhost\",\"password\":\"password1\"}' http://localhost:5001/platform/authorization`
- Auth status: `curl -s -b /tmp/doorman_cookies.txt http://localhost:5001/platform/authorization/status`
- One-liner: `BASE_URL=http://localhost:5001 STARTUP_ADMIN_EMAIL=admin@localhost STARTUP_ADMIN_PASSWORD=password1 bash scripts/smoke.sh`
Production notes
@@ -186,8 +186,8 @@ Quick go-live checklist
- `curl -s http://localhost:5001/platform/monitor/liveness``{ "status": "alive" }`
- `curl -s http://localhost:5001/platform/monitor/readiness``{ status: "ready", ... }`
- Smoke auth:
- `curl -s -c cookies.txt -H 'Content-Type: application/json' -d '{"email":"admin@localhost","password":"password1"}' http://localhost:5001/platform/authorization`
- `curl -s -b cookies.txt http://localhost:5001/platform/authorization/status`
- `curl -s -c /tmp/doorman_cookies.txt -H 'Content-Type: application/json' -d '{\"email\":\"admin@localhost\",\"password\":\"password1\"}' http://localhost:5001/platform/authorization`
- `curl -s -b /tmp/doorman_cookies.txt http://localhost:5001/platform/authorization/status`
- Web: Ensure `web-client/.env.local` has `NEXT_PUBLIC_SERVER_URL=http://localhost:5001`, then `npm run build && npm start` (or use compose service `web`).
Optional: run `bash scripts/smoke.sh` (uses `BASE_URL`, `STARTUP_ADMIN_EMAIL`, `STARTUP_ADMIN_PASSWORD`).
-6
View File
@@ -1,6 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_.localhost TRUE / FALSE 1758337273 access_token_cookie eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTc1ODMzNzI3MywianRpIjoiNGY1NGU0MDUtN2Q4Mi00NDE5LTg1NmQtNDUzYWUyMmNhMjNhIiwiYWNjZXNzZXMiOnsidWlfYWNjZXNzIjp0cnVlLCJtYW5hZ2VfdXNlcnMiOnRydWUsIm1hbmFnZV9hcGlzIjp0cnVlLCJtYW5hZ2VfZW5kcG9pbnRzIjp0cnVlLCJtYW5hZ2VfZ3JvdXBzIjp0cnVlLCJtYW5hZ2Vfcm9sZXMiOnRydWUsIm1hbmFnZV9yb3V0aW5ncyI6dHJ1ZSwibWFuYWdlX2dhdGV3YXkiOnRydWUsIm1hbmFnZV9zdWJzY3JpcHRpb25zIjp0cnVlLCJtYW5hZ2Vfc2VjdXJpdHkiOnRydWUsImV4cG9ydF9sb2dzIjp0cnVlLCJ2aWV3X2xvZ3MiOnRydWV9fQ.oQSyvnu4raM2X0C18H6Evr-_UgZ8rGbAHrtQNGxJTcI
.localhost TRUE / FALSE 1758337273 csrf_token 17996394-05ab-4cb2-a86b-ecc3b72f071d
+1 -1
View File
@@ -52,7 +52,7 @@ async def get_dashboard_data(request: Request):
# Active users list from top_users in metrics; enrich with subscribers (count of apis in subscriptions)
active_users_list = []
for username, reqs in snap.get('top_users', [])[:10]:
for username, reqs in snap.get('top_users', [])[:5]:
subs = subscriptions_collection.find_one({'username': username}) or {}
subscribers = len(subs.get('apis', [])) if isinstance(subs.get('apis'), list) else 0
active_users_list.append({
+7 -1
View File
@@ -4,7 +4,13 @@ Review the Apache License 2.0 for valid authorization of use
See https://github.com/pypeople-dev/doorman for more information
"""
from datetime import datetime, timedelta, UTC
from datetime import datetime, timedelta
try:
# Python 3.11+
from datetime import UTC # type: ignore
except Exception: # Python <3.11 fallback
from datetime import timezone as _timezone # type: ignore
UTC = _timezone.utc # type: ignore
import os
import uuid
from fastapi import HTTPException, Request
+107
View File
@@ -0,0 +1,107 @@
'use client'
import React, { useState } from 'react'
import Layout from '@/components/Layout'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { SERVER_URL } from '@/utils/config'
import { fetchJson } from '@/utils/http'
export default function DemoSeedPage() {
const [users, setUsers] = useState(60)
const [apis, setApis] = useState(20)
const [endpoints, setEndpoints] = useState(6)
const [groups, setGroups] = useState(10)
const [protos, setProtos] = useState(6)
const [logs, setLogs] = useState(2000)
const [seed, setSeed] = useState<string>('')
const [working, setWorking] = useState(false)
const [error, setError] = useState<string | null>(null)
const [result, setResult] = useState<any>(null)
const run = async () => {
try {
setWorking(true); setError(null); setResult(null)
const params = new URLSearchParams({
users: String(users), apis: String(apis), endpoints: String(endpoints), groups: String(groups), protos: String(protos), logs: String(logs)
})
if (seed.trim()) params.set('seed', seed.trim())
const url = `${SERVER_URL}/platform/demo/seed?${params.toString()}`
const res = await fetchJson<any>(url, { method: 'POST' })
setResult(res)
} catch (e:any) {
setError(e?.message || 'Failed to seed demo data')
} finally {
setWorking(false)
}
}
return (
<ProtectedRoute requiredPermission="manage_gateway">
<Layout>
<div className="space-y-6">
<div className="page-header">
<div>
<h1 className="page-title">Demo Data Seeder</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Populate the running server with demo users, APIs, endpoints, tokens, logs, and metrics</p>
</div>
<button className="btn btn-primary" onClick={run} disabled={working}>{working ? 'Seeding…' : 'Run Seeder'}</button>
</div>
<div className="card">
<div className="p-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium">Users</label>
<input className="input" type="number" value={users} onChange={e => setUsers(Number(e.target.value || 0))} />
</div>
<div>
<label className="block text-sm font-medium">APIs</label>
<input className="input" type="number" value={apis} onChange={e => setApis(Number(e.target.value || 0))} />
</div>
<div>
<label className="block text-sm font-medium">Endpoints / API</label>
<input className="input" type="number" value={endpoints} onChange={e => setEndpoints(Number(e.target.value || 0))} />
</div>
<div>
<label className="block text-sm font-medium">Groups</label>
<input className="input" type="number" value={groups} onChange={e => setGroups(Number(e.target.value || 0))} />
</div>
<div>
<label className="block text-sm font-medium">Protos</label>
<input className="input" type="number" value={protos} onChange={e => setProtos(Number(e.target.value || 0))} />
</div>
<div>
<label className="block text-sm font-medium">Logs</label>
<input className="input" type="number" value={logs} onChange={e => setLogs(Number(e.target.value || 0))} />
</div>
<div className="md:col-span-3">
<label className="block text-sm font-medium">RNG Seed (optional)</label>
<input className="input" value={seed} onChange={e => setSeed(e.target.value)} placeholder="e.g. 12345" />
</div>
</div>
</div>
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3"><p className="text-sm text-error-700">{error}</p></div>
</div>
</div>
)}
{result && (
<div className="card">
<div className="card-header"><h3 className="card-title">Seed Results</h3></div>
<div className="p-6">
<pre className="text-xs whitespace-pre-wrap">{JSON.stringify(result, null, 2)}</pre>
</div>
</div>
)}
</div>
</Layout>
</ProtectedRoute>
)
}
+10 -1
View File
@@ -121,6 +121,15 @@
.main-content {
@apply lg:ml-64 min-h-screen bg-gray-50 dark:bg-dark-bg;
}
/* Hide scrollbars but allow scroll (sidebar/menu) */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Page header */
.page-header {
@@ -179,4 +188,4 @@
.gradient-text {
@apply bg-gradient-to-r from-primary-600 to-primary-800 bg-clip-text text-transparent;
}
}
}
+14 -9
View File
@@ -37,11 +37,12 @@ const MonitorPage: React.FC = () => {
fetchProbes()
}, [timeRange])
const fetchMetrics = async () => {
const fetchMetrics = async (rangeOverride?: string) => {
try {
setLoading(true)
setError(null)
const payload = await getJson<any>(`${SERVER_URL}/platform/monitor/metrics?range=${encodeURIComponent(timeRange)}`)
const range = rangeOverride ?? timeRange
const payload = await getJson<any>(`${SERVER_URL}/platform/monitor/metrics?range=${encodeURIComponent(range)}`)
setMetrics(payload)
} catch (err) {
if (err instanceof Error) {
@@ -102,7 +103,7 @@ const MonitorPage: React.FC = () => {
<div className="flex gap-2">
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
onChange={(e) => { const v = e.target.value; setTimeRange(v); fetchMetrics(v) }}
className="input"
>
<option value="1h">Last Hour</option>
@@ -260,12 +261,16 @@ const MonitorPage: React.FC = () => {
<div className="p-6">
<div className="h-48 overflow-y-auto">
<ul className="text-sm space-y-1">
{(metrics?.series || []).slice().reverse().map((pt: any) => (
<li key={pt.timestamp} className="flex justify-between">
<span>{new Date(pt.timestamp * 1000).toLocaleTimeString()}</span>
<span>{pt.count} req avg {Math.round(pt.avg_ms)}ms {pt.error_count} errs</span>
</li>
))}
{Array.from((metrics?.series || []).entries())
.reverse()
.map(([idx, pt]: [number, any]) => (
<li key={`${pt.timestamp}-${idx}`} className="flex justify-between">
<span>{new Date(pt.timestamp * 1000).toLocaleTimeString()}</span>
<span>
{pt.count} req avg {Math.round(pt.avg_ms)}ms {pt.error_count} errs
</span>
</li>
))}
</ul>
{(!metrics || !metrics.series || metrics.series.length === 0) && (
<p className="text-gray-500 dark:text-gray-400">No data</p>
+12 -11
View File
@@ -26,6 +26,7 @@ const menuItems: MenuItem[] = [
{ label: 'Routings', href: '/routings', icon: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4', permission: 'manage_routings' },
{ label: 'Logging', href: '/logging', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2', permission: 'view_logs' },
{ label: 'Monitor', href: '/monitor', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', permission: 'manage_gateway' },
{ label: 'Demo Seed', href: '/demo', icon: 'M12 4v16m8-8H4', permission: 'manage_gateway' },
{ label: 'Tokens', href: '/tokens', icon: 'M12 8c-1.657 0-3 1.343-3 3 0 2.239 3 5 3 5s3-2.761 3-5c0-1.657-1.343-3-3-3z M12 13a2 2 0 110-4 2 2 0 010 4z', permission: 'manage_tokens' },
{ label: 'Token Definitions', href: '/token-defs', icon: 'M5 13l4 4L19 7', permission: 'manage_tokens' },
{ label: 'Subscriptions', href: '/subscriptions', icon: 'M3 5h12M9 3v2m-6 4h12M9 9v2m-6 4h12m-6 0v2', permission: 'manage_subscriptions' },
@@ -83,26 +84,26 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
<aside className={`sidebar ${sidebarOpen ? 'translate-x-0' : ''}`}>
<div className="flex h-full flex-col">
{/* Logo */}
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">Doorman</h1>
</div>
{/* Menu Items */}
<div className="flex-1 p-4">
<nav className="space-y-2">
<div className="flex-1 p-3 overflow-y-auto no-scrollbar">
<nav className="space-y-1">
{filteredMenuItems.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
className={`flex items-center px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
isActive
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white'
}`}
>
<svg className="mr-3 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
</svg>
{item.label}
@@ -113,19 +114,19 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
</div>
{/* Bottom Actions */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 space-y-2">
<div className="p-3 border-t border-gray-200 dark:border-gray-700 space-y-1.5">
{/* Dark mode toggle */}
<button
onClick={toggleTheme}
className="flex items-center w-full px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700 rounded-md transition-colors"
className="flex items-center w-full px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700 rounded-md transition-colors"
aria-label="Toggle dark mode"
>
{theme === 'light' ? (
<svg className="mr-3 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="mr-3 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
@@ -135,9 +136,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
{user && (
<button
onClick={logout}
className="flex items-center w-full px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700 rounded-md transition-colors"
className="flex items-center w-full px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700 rounded-md transition-colors"
>
<svg className="mr-3 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Logout