feat!: move config & settings under /api/v1/environments/{envSlug}/...
P3A of the taxonomy migration. Env-scoped config and settings endpoints
now live under the env-prefixed URL shape, making env a first-class
path segment instead of a query param. Agent-authoritative config is
split off into a dedicated endpoint so agent env comes from the JWT
only — never spoofable via URL.
Server:
- ApplicationConfigController: @RequestMapping("/api/v1/environments/
{envSlug}"). Handlers use @EnvPath Environment env, appSlug as
@PathVariable. Removed the dual-mode resolveEnvironmentForRead —
user flow only; agent flow moved to AgentConfigController.
- AgentConfigController (new): GET /api/v1/agents/config. Reads
instanceId from JWT subject, resolves (app, env) from registry,
returns AppConfigResponse. Registry miss → falls back to JWT env
claim for environment, but 404s if application cannot be derived
(no other source without registry).
- AppSettingsController: @RequestMapping("/api/v1/environments/
{envSlug}"). List at /app-settings, per-app at /apps/{appSlug}/
settings. Access class-wide PreAuthorize preserved (ADMIN/OPERATOR).
SPA:
- commands.ts: useAllApplicationConfigs, useApplicationConfig,
useUpdateApplicationConfig, useProcessorRouteMapping,
useTestExpression — rewritten URLs to /environments/{env}/apps/
{app}/... shape. environment now required on every call. Query
keys include environment so cache is env-scoped.
- dashboard.ts: useAppSettings, useAllAppSettings, useUpdateAppSettings
rewritten.
- TapConfigModal: new required environment prop; callers updated.
- RouteDetail, ExchangesPage: thread selectedEnv into test-expression
and modal.
Config changes in SecurityConfig for the new shape landed earlier in
P0.2; no security rule changes needed in this commit.
BREAKING CHANGE: /api/v1/config/** and /api/v1/admin/app-settings/**
paths removed. Agents must use /api/v1/agents/config instead of
GET /api/v1/config/{app}; users must use /api/v1/environments/{env}/
apps/{app}/config and /api/v1/environments/{env}/apps/{app}/settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,23 +45,24 @@ function authFetch(path: string, init?: RequestInit): Promise<Response> {
|
||||
return fetch(`${config.apiBaseUrl}${path}`, { ...init, headers })
|
||||
}
|
||||
|
||||
export function useAllApplicationConfigs() {
|
||||
export function useAllApplicationConfigs(environment: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ['applicationConfig', 'all'],
|
||||
queryKey: ['applicationConfig', 'all', environment],
|
||||
queryFn: async () => {
|
||||
const res = await authFetch('/config')
|
||||
const res = await authFetch(`/environments/${encodeURIComponent(environment!)}/config`)
|
||||
if (!res.ok) throw new Error('Failed to fetch configs')
|
||||
return res.json() as Promise<ApplicationConfig[]>
|
||||
},
|
||||
enabled: !!environment,
|
||||
})
|
||||
}
|
||||
|
||||
export function useApplicationConfig(application: string | undefined, environment: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ['applicationConfig', application, environment],
|
||||
queryKey: ['applicationConfig', environment, application],
|
||||
queryFn: async () => {
|
||||
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
|
||||
const res = await authFetch(`/config/${application}${envParam}`)
|
||||
const res = await authFetch(
|
||||
`/environments/${encodeURIComponent(environment!)}/apps/${encodeURIComponent(application!)}/config`)
|
||||
if (!res.ok) throw new Error(`Failed to fetch config: ${res.status}`)
|
||||
const data = await res.json()
|
||||
// Server returns AppConfigResponse: { config, globalSensitiveKeys, mergedSensitiveKeys }
|
||||
@@ -82,9 +83,9 @@ export interface ConfigUpdateResponse {
|
||||
export function useUpdateApplicationConfig() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async ({ config, environment }: { config: ApplicationConfig; environment?: string }) => {
|
||||
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
|
||||
const res = await authFetch(`/config/${config.application}${envParam}`, {
|
||||
mutationFn: async ({ config, environment }: { config: ApplicationConfig; environment: string }) => {
|
||||
const res = await authFetch(
|
||||
`/environments/${encodeURIComponent(environment)}/apps/${encodeURIComponent(config.application)}/config`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
@@ -92,9 +93,9 @@ export function useUpdateApplicationConfig() {
|
||||
if (!res.ok) throw new Error('Failed to update config')
|
||||
return res.json() as Promise<ConfigUpdateResponse>
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
queryClient.setQueryData(['applicationConfig', result.config.application], result.config)
|
||||
queryClient.invalidateQueries({ queryKey: ['applicationConfig', 'all'] })
|
||||
onSuccess: (result, vars) => {
|
||||
queryClient.setQueryData(['applicationConfig', vars.environment, result.config.application], result.config)
|
||||
queryClient.invalidateQueries({ queryKey: ['applicationConfig'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -103,10 +104,10 @@ export function useUpdateApplicationConfig() {
|
||||
|
||||
export function useProcessorRouteMapping(application?: string, environment?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['config', application, environment, 'processor-routes'],
|
||||
queryKey: ['config', environment, application, 'processor-routes'],
|
||||
queryFn: async () => {
|
||||
const res = await authFetch(
|
||||
`/config/${application}/processor-routes?environment=${encodeURIComponent(environment!)}`)
|
||||
`/environments/${encodeURIComponent(environment!)}/apps/${encodeURIComponent(application!)}/processor-routes`)
|
||||
if (!res.ok) throw new Error('Failed to fetch processor-route mapping')
|
||||
return res.json() as Promise<Record<string, string>>
|
||||
},
|
||||
@@ -154,19 +155,21 @@ export function useTestExpression() {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
application,
|
||||
environment,
|
||||
expression,
|
||||
language,
|
||||
body,
|
||||
target,
|
||||
}: {
|
||||
application: string
|
||||
environment: string
|
||||
expression: string
|
||||
language: string
|
||||
body: string
|
||||
target: string
|
||||
}) => {
|
||||
const res = await authFetch(
|
||||
`/config/${encodeURIComponent(application)}/test-expression`,
|
||||
`/environments/${encodeURIComponent(environment)}/apps/${encodeURIComponent(application)}/config/test-expression`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -127,9 +127,9 @@ export interface AppSettings {
|
||||
|
||||
export function useAppSettings(appId?: string, environment?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['app-settings', appId, environment],
|
||||
queryKey: ['app-settings', environment, appId],
|
||||
queryFn: () => fetchJson<AppSettings>(
|
||||
`/admin/app-settings/${appId}?environment=${encodeURIComponent(environment!)}`),
|
||||
`/environments/${encodeURIComponent(environment!)}/apps/${encodeURIComponent(appId!)}/settings`),
|
||||
enabled: !!appId && !!environment,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
@@ -139,7 +139,7 @@ export function useAllAppSettings(environment?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['app-settings', 'all', environment],
|
||||
queryFn: () => fetchJson<AppSettings[]>(
|
||||
`/admin/app-settings?environment=${encodeURIComponent(environment!)}`),
|
||||
`/environments/${encodeURIComponent(environment!)}/app-settings`),
|
||||
enabled: !!environment,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
@@ -151,7 +151,7 @@ export function useUpdateAppSettings() {
|
||||
mutationFn: async ({ appId, environment, settings }:
|
||||
{ appId: string; environment: string; settings: Omit<AppSettings, 'appId' | 'createdAt' | 'updatedAt'> }) => {
|
||||
const res = await fetch(
|
||||
`${config.apiBaseUrl}/admin/app-settings/${appId}?environment=${encodeURIComponent(environment)}`,
|
||||
`${config.apiBaseUrl}/environments/${encodeURIComponent(environment)}/apps/${encodeURIComponent(appId)}/settings`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface TapConfigModalProps {
|
||||
defaultProcessorId?: string;
|
||||
/** Application name (for test expression API) */
|
||||
application: string;
|
||||
/** Environment slug (for test expression API) */
|
||||
environment: string;
|
||||
/** Current application config (taps array will be modified) */
|
||||
config: ApplicationConfig;
|
||||
/** Called with the updated config to persist */
|
||||
@@ -53,7 +55,7 @@ export interface TapConfigModalProps {
|
||||
|
||||
export function TapConfigModal({
|
||||
open, onClose, tap, processorOptions, defaultProcessorId,
|
||||
application, config, onSave, onDelete,
|
||||
application, environment, config, onSave, onDelete,
|
||||
}: TapConfigModalProps) {
|
||||
const isEdit = !!tap;
|
||||
|
||||
@@ -125,7 +127,7 @@ export function TapConfigModal({
|
||||
|
||||
function handleTest() {
|
||||
testMutation.mutate(
|
||||
{ application, expression, language, body: testPayload, target },
|
||||
{ application, environment, expression, language, body: testPayload, target },
|
||||
{
|
||||
onSuccess: (data) => setTestResult(data),
|
||||
onError: (err) => setTestResult({ error: (err as Error).message }),
|
||||
|
||||
@@ -322,6 +322,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
||||
processorOptions={processorOptions}
|
||||
defaultProcessorId={tapModalTarget}
|
||||
application={appId}
|
||||
environment={selectedEnv}
|
||||
config={appConfig}
|
||||
onSave={handleTapSave}
|
||||
onDelete={handleTapDelete}
|
||||
|
||||
@@ -573,7 +573,7 @@ export default function RouteDetail() {
|
||||
if (!appId) return;
|
||||
const body = testTab === 'recent' ? testExchangeId : testPayload;
|
||||
testExpressionMutation.mutate(
|
||||
{ application: appId, expression: tapExpression, language: tapLanguage, body, target: tapTarget },
|
||||
{ application: appId, environment: selectedEnv, expression: tapExpression, language: tapLanguage, body, target: tapTarget },
|
||||
{ onSuccess: (data) => setTestResult(data), onError: (err) => setTestResult({ error: (err as Error).message }) },
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user