Contract-first API with DTOs, validation, and server-side OpenAPI post-processing
All checks were successful
CI / build (push) Successful in 1m27s
CI / docker (push) Successful in 2m6s
CI / deploy (push) Successful in 30s

Add dedicated request/response DTOs for all controllers, replacing raw
JsonNode parameters with validated types. Move OpenAPI path-prefix stripping
and ProcessorNode children injection into OpenApiCustomizer beans so the
spec served at /api/v1/api-docs is already clean — eliminating the need for
the ui/scripts/process-openapi.mjs post-processing script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-14 15:33:37 +01:00
parent 50bb22d6f6
commit 465f210aee
43 changed files with 1561 additions and 509 deletions

View File

@@ -1,7 +1,7 @@
import { type FormEvent, useEffect, useState } from 'react';
import { Navigate } from 'react-router';
import { useAuthStore } from './auth-store';
import { config } from '../config';
import { api } from '../api/client';
import styles from './LoginPage.module.css';
interface OidcInfo {
@@ -17,9 +17,8 @@ export function LoginPage() {
const [oidcLoading, setOidcLoading] = useState(false);
useEffect(() => {
fetch(`${config.apiBaseUrl}/auth/oidc/config`)
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
api.GET('/auth/oidc/config')
.then(({ data }) => {
if (data?.authorizationEndpoint && data?.clientId) {
setOidc({ clientId: data.clientId, authorizationEndpoint: data.authorizationEndpoint });
if (data.endSessionEndpoint) {

View File

@@ -1,5 +1,5 @@
import { create } from 'zustand';
import { config } from '../config';
import { api } from '../api/client';
interface AuthState {
accessToken: string | null;
@@ -58,16 +58,13 @@ export const useAuthStore = create<AuthState>((set, get) => ({
login: async (username, password) => {
set({ loading: true, error: null });
try {
const res = await fetch(`${config.apiBaseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
const { data, error } = await api.POST('/auth/login', {
body: { username, password },
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.message || 'Invalid credentials');
if (error || !data) {
throw new Error('Invalid credentials');
}
const { accessToken, refreshToken } = await res.json();
const { accessToken, refreshToken } = data;
localStorage.removeItem('cameleer-oidc-end-session');
persistTokens(accessToken, refreshToken, username);
set({
@@ -89,16 +86,13 @@ export const useAuthStore = create<AuthState>((set, get) => ({
loginWithOidcCode: async (code, redirectUri) => {
set({ loading: true, error: null });
try {
const res = await fetch(`${config.apiBaseUrl}/auth/oidc/callback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, redirectUri }),
const { data, error } = await api.POST('/auth/oidc/callback', {
body: { code, redirectUri },
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.message || 'OIDC login failed');
if (error || !data) {
throw new Error('OIDC login failed');
}
const { accessToken, refreshToken } = await res.json();
const { accessToken, refreshToken } = data;
const payload = JSON.parse(atob(accessToken.split('.')[1]));
const username = payload.sub ?? 'oidc-user';
persistTokens(accessToken, refreshToken, username);
@@ -122,13 +116,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
const { refreshToken } = get();
if (!refreshToken) return false;
try {
const res = await fetch(`${config.apiBaseUrl}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
const { data, error } = await api.POST('/auth/refresh', {
body: { refreshToken },
});
if (!res.ok) return false;
const data = await res.json();
if (error || !data) return false;
const username = get().username ?? '';
persistTokens(data.accessToken, data.refreshToken, username);
set({