mirror of
https://github.com/apidoorman/doorman.git
synced 2026-05-13 04:18:26 -05:00
pre release changes
This commit is contained in:
+31
@@ -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
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user