Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ services:
WEB_PORT: 3000
restart: "no"
network_mode: "host"
networks: !reset []
volumes:
- type: tmpfs
target: /app/data
Expand Down
17 changes: 17 additions & 0 deletions user-docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ Release Candidate Runbook
deployment is not required to launch. The Python-to-Rust steps below are
compatibility rehearsals using synthetic fixtures, not production migration
prerequisites. V2 backup, restore, and deployment recovery still require proof.
- Behavior changes from the Python gateway (review before reusing a v1 configuration):
- Tier and rate-limit-rule management now require the `manage_tiers` /
`manage_rate_limits` permissions (Python left them unauthenticated). Grant
these to any automation that calls those APIs.
- Gateway subscription bypass applies only to platform-admin roles, not to
every role with `manage_gateway`.
- JWT `iss`/`aud` claims are always required and expiry has no leeway.
- gRPC-Web requests run the full authentication and policy chain.
- `MEM_AUTO_SAVE_ENABLED` is honored; memory-mode deployments that relied on
Python's unconditional periodic dumps must set it to `true`.
- An invalid `JWT_KEYS` value refuses startup instead of falling back to
`JWT_SECRET_KEY`.
- `GATEWAY_TIMEOUT`, `RETRY_ENABLED`, and `RETRY_MAX_ATTEMPTS` are applied.
- MongoDB: on first start the gateway replaces the `apis.api_id_1` unique
index with a partial unique index (`api_id_present_1`) so APIs without an
`api_id` no longer collide. No manual step is needed; the account needs
index-management privileges on the database.
- Scope: Run this against the exact immutable image digest proposed for release,
a production-like MongoDB replica set, and a separate Redis instance. Keep
the generated artifacts under `release-evidence/` and do not overwrite a
Expand Down
10 changes: 5 additions & 5 deletions web-client/public/android-chrome-192x192.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions web-client/public/android-chrome-512x512.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions web-client/public/apple-touch-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions web-client/public/doorman-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions web-client/public/doorman-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions web-client/public/favicon-16x16.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions web-client/public/favicon-32x32.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions web-client/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions web-client/public/safari-pinned-tab.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
185 changes: 5 additions & 180 deletions web-client/src/app/apis/[apiId]/endpoints/add/page.tsx
Original file line number Diff line number Diff line change
@@ -1,187 +1,12 @@
'use client'

import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
import InfoTooltip from '@/components/InfoTooltip'
import FormHelp from '@/components/FormHelp'
import { SERVER_URL } from '@/utils/config'
import { postJson } from '@/utils/api'

export default function AddEndpointPage() {
// Endpoints are added inline from the endpoints table; keep this route so old links still work.
export default function AddEndpointRedirect() {
const params = useParams()
const router = useRouter()
const apiId = params.apiId as string
const [apiName, setApiName] = useState('')
const [apiVersion, setApiVersion] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)

const [method, setMethod] = useState('GET')
const [uri, setUri] = useState('')
const [clientUri, setClientUri] = useState('')
const [description, setDescription] = useState('')
const [useOverride, setUseOverride] = useState(false)
const [servers, setServers] = useState<string[]>([])
const [newServer, setNewServer] = useState('')

useEffect(() => {
try {
const apiData = sessionStorage.getItem('selectedApi')
if (apiData) {
const parsed = JSON.parse(apiData)
setApiName(parsed.api_name || '')
setApiVersion(parsed.api_version || '')
}
} catch { }
}, [])

const addServer = () => {
const v = newServer.trim()
if (!v) return
if (servers.includes(v)) return
setServers(prev => [...prev, v])
setNewServer('')
}

const removeServer = (idx: number) => {
setServers(prev => prev.filter((_, i) => i !== idx))
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!uri.trim()) return
setLoading(true)
setError(null)
try {
const body: any = {
api_name: apiName,
api_version: apiVersion,
endpoint_method: method,
endpoint_uri: uri.startsWith('/') ? uri : '/' + uri,
endpoint_description: description || `${method} ${uri}`
}
if (clientUri.trim()) {
body.client_uri = clientUri.trim()
}
if (useOverride && servers.length > 0) {
body.endpoint_servers = servers
}
await postJson(`${SERVER_URL}/platform/endpoint`, body)
setSuccess('Endpoint created')
setTimeout(() => setSuccess(null), 1500)
router.push(`/apis/${encodeURIComponent(apiId)}/endpoints`)
} catch (e: any) {
setError(e?.message || 'Failed to create endpoint')
} finally {
setLoading(false)
}
}

return (
<Layout>
<div className="space-y-6">
<div className="page-header">
<div>
<h1 className="page-title">Add Endpoint</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">For API {apiName}/{apiVersion}</p>
</div>
<div className="flex gap-2">
<Link href={`/apis/${encodeURIComponent(apiId)}/endpoints`} className="btn btn-secondary">Back to Endpoints</Link>
</div>
</div>

{success && (
<div className="rounded-lg bg-success-50 border border-success-200 p-4 dark:bg-success-900/20 dark:border-success-800">
<div className="flex"><p className="text-sm text-success-700 dark:text-success-300">{success}</p></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"><p className="text-sm text-error-700 dark:text-error-300">{error}</p></div>
</div>
)}

<div className="card max-w-3xl">
<form onSubmit={handleSubmit} className="p-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2 -mt-2 mb-2">
<FormHelp docHref="/docs/using-fields.html#endpoints">Define method and URI; optional upstream override per endpoint.</FormHelp>
</div>
<div>
<label className="block text-sm font-medium mb-1">Method</label>
<select className="input" value={method} onChange={e => setMethod(e.target.value)}>
{['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">
URI
<InfoTooltip text="Path pattern relative to the API base. Use {param} for path variables. Example: /items/{id}" />
</label>
<input className="input" value={uri} onChange={e => setUri(e.target.value)} placeholder="/backend/path/{id}" />
</div>
<div>
<label className="block text-sm font-medium mb-1">
Client URI (Optional)
<InfoTooltip text="Public-facing URI if different from backend URI." />
</label>
<input className="input" value={clientUri} onChange={e => setClientUri(e.target.value)} placeholder="/public/path/{id}" />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-1">Description</label>
<input className="input" value={description} onChange={e => setDescription(e.target.value)} placeholder="Describe endpoint" />
</div>

<div className="md:col-span-2">
<div className="flex items-center gap-2 mb-2">
<input id="use-override" type="checkbox" className="h-4 w-4" checked={useOverride} onChange={(e) => setUseOverride(e.target.checked)} />
<label htmlFor="use-override" className="text-sm">
Use endpoint servers (override API servers)
<InfoTooltip text="Provide endpoint-specific upstreams. If disabled or empty, the API-level servers are used." />
</label>
</div>
<div className={`flex gap-2 ${useOverride ? '' : 'opacity-60'}`}>
<input
className="input flex-1"
value={newServer}
onChange={e => setNewServer(e.target.value)}
placeholder="e.g., http://localhost:8082"
onKeyPress={(e) => useOverride && e.key === 'Enter' && addServer()}
disabled={!useOverride}
/>
<button type="button" onClick={addServer} className="btn btn-secondary" disabled={!useOverride}>Add</button>
</div>
<div className={`mt-2 space-y-2 ${useOverride ? '' : 'opacity-60'}`}>
{servers.map((srv, idx) => (
<div key={idx} className="flex items-center justify-between bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded">
<span className="text-sm font-mono">{srv}</span>
<button type="button" onClick={() => removeServer(idx)} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" disabled={!useOverride}>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
{!useOverride && (
<p className="text-xs text-gray-500 dark:text-gray-400">Disabled — API servers will be used unless enabled.</p>
)}
{useOverride && servers.length === 0 && (
<p className="text-xs text-gray-500 dark:text-gray-400">No endpoint-specific servers. API servers will be used.</p>
)}
</div>
</div>

<div className="md:col-span-2">
<button type="submit" className="btn btn-primary" disabled={loading || !uri.trim()}>
{loading ? <div className="flex items-center"><div className="spinner mr-2"></div>Creating...</div> : 'Create Endpoint'}
</button>
<Link href={`/apis/${encodeURIComponent(apiId)}/endpoints`} className="btn btn-ghost ml-2">Cancel</Link>
</div>
</form>
</div>
</div>
</Layout>
)
useEffect(() => { router.replace(`/apis/${encodeURIComponent(params.apiId as string)}/endpoints?add=1`) }, [params.apiId, router])
return null
}
Loading
Loading