Replace hardcoded .env configuration with database-backed settings
manageable through the Admin web interface. This reduces .env to
bootstrap-only variables (DB, Keycloak, encryption keys).
Backend:
- Add SystemSetting Prisma model with category, valueType, isSecret
- Add system-settings NestJS module (CRUD, 60s cache, encryption)
- Refactor all 7 connectors to lazy-load credentials from DB via
CredentialsService.findActiveByType() instead of ConfigService
- Add event-driven credential reload (@nestjs/event-emitter)
- Dynamic CORS origins and conditional Swagger from DB settings
- Fix JWT strategy: use Keycloak JWKS (RS256) instead of symmetric secret
- Add SYSTEM_SETTINGS_VIEW/MANAGE permissions
- Seed 13 default settings (sync intervals, features, branding, CORS)
- Add env-to-db migration script (prisma/migrate-env-to-db.ts)
Frontend:
- Add use-credentials hook (full CRUD for integration credentials)
- Add use-system-settings hook (read/update system settings)
- Wire admin-integrations page to real API (create/update/test/toggle)
- Add admin system-settings page with 4 tabs (Branding, CORS, Sync, Features)
- Fix sidebar double-highlighting with exactMatch flag
- Fix integration detail fallback when API unavailable
- Fix API client to unwrap backend's {success, data} envelope
- Update NEXT_PUBLIC_API_URL to include /v1 version prefix
- Fix activity-widget hydration error
- Add i18n keys for systemSettings (de + en)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
3.0 KiB
TypeScript
126 lines
3.0 KiB
TypeScript
import { getSession } from 'next-auth/react';
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
|
|
|
|
/**
|
|
* API client configuration
|
|
*/
|
|
interface RequestConfig extends RequestInit {
|
|
params?: Record<string, string | number | boolean | undefined>;
|
|
}
|
|
|
|
/**
|
|
* Build URL with query parameters
|
|
*/
|
|
function buildUrl(endpoint: string, params?: RequestConfig['params']): string {
|
|
const url = new URL(`${API_URL}${endpoint}`);
|
|
|
|
if (params) {
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
url.searchParams.append(key, String(value));
|
|
}
|
|
});
|
|
}
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
/**
|
|
* Make an authenticated API request
|
|
*/
|
|
async function request<T>(endpoint: string, config: RequestConfig = {}): Promise<T> {
|
|
const { params, ...init } = config;
|
|
|
|
// Get session for auth token
|
|
const session = await getSession();
|
|
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
...(init.headers || {}),
|
|
};
|
|
|
|
// Add auth header if session exists
|
|
if (session?.accessToken) {
|
|
(headers as Record<string, string>)['Authorization'] = `Bearer ${session.accessToken}`;
|
|
}
|
|
|
|
const response = await fetch(buildUrl(endpoint, params), {
|
|
...init,
|
|
headers,
|
|
});
|
|
|
|
// Handle non-OK responses
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ message: 'An error occurred' }));
|
|
throw new Error(error.message || `HTTP error ${response.status}`);
|
|
}
|
|
|
|
// Handle empty responses
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType?.includes('application/json')) {
|
|
const json = await response.json();
|
|
// Unwrap backend's {success, data, timestamp} envelope
|
|
if (json && typeof json === 'object' && 'success' in json && 'data' in json) {
|
|
return json.data as T;
|
|
}
|
|
return json as T;
|
|
}
|
|
|
|
return {} as T;
|
|
}
|
|
|
|
/**
|
|
* API client with typed methods
|
|
*/
|
|
export const api = {
|
|
/**
|
|
* GET request
|
|
*/
|
|
get<T>(endpoint: string, config?: RequestConfig): Promise<T> {
|
|
return request<T>(endpoint, { ...config, method: 'GET' });
|
|
},
|
|
|
|
/**
|
|
* POST request
|
|
*/
|
|
post<T>(endpoint: string, data?: unknown, config?: RequestConfig): Promise<T> {
|
|
return request<T>(endpoint, {
|
|
...config,
|
|
method: 'POST',
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* PUT request
|
|
*/
|
|
put<T>(endpoint: string, data?: unknown, config?: RequestConfig): Promise<T> {
|
|
return request<T>(endpoint, {
|
|
...config,
|
|
method: 'PUT',
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* PATCH request
|
|
*/
|
|
patch<T>(endpoint: string, data?: unknown, config?: RequestConfig): Promise<T> {
|
|
return request<T>(endpoint, {
|
|
...config,
|
|
method: 'PATCH',
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* DELETE request
|
|
*/
|
|
delete<T>(endpoint: string, config?: RequestConfig): Promise<T> {
|
|
return request<T>(endpoint, { ...config, method: 'DELETE' });
|
|
},
|
|
};
|
|
|
|
export default api;
|