Files
SRCmail/hooks/use-config.ts
T
Linus Rath ab72fc06ff feat: add Stalwart account security management
- Add Stalwart API client library (lib/stalwart/client.ts)
- Add server-side proxy routes for auth, crypto, password, principal, probe
- Add account security Zustand store with full state management
- Add Security settings tab with password change, display name, TOTP 2FA,
  app passwords, and encryption-at-rest controls
- Add stalwartFeaturesEnabled config flag (opt-out via STALWART_FEATURES=false)
- Add i18n translations for all 8 locales (en, de, es, fr, it, ja, nl, pt)
- Add tests for Stalwart client (24 tests) and security store (29 tests)
2026-03-12 02:20:56 +01:00

124 lines
3.3 KiB
TypeScript

"use client";
import { useState, useEffect } from 'react';
interface ConfigData {
appName: string;
jmapServerUrl: string;
oauthEnabled: boolean;
oauthClientId: string;
oauthIssuerUrl: string;
rememberMeEnabled: boolean;
settingsSyncEnabled: boolean;
stalwartFeaturesEnabled: boolean;
devMode: boolean;
}
interface AppConfig extends ConfigData {
isLoading: boolean;
error: string | null;
}
let configCache: ConfigData | null = null;
let configPromise: Promise<ConfigData> | null = null;
async function fetchConfig(): Promise<ConfigData> {
// Return cached config if available
if (configCache) {
return configCache;
}
// If a fetch is already in progress, wait for it
if (configPromise) {
return configPromise;
}
// Start a new fetch
configPromise = fetch('/api/config')
.then((res) => {
if (!res.ok) {
throw new Error('Failed to fetch config');
}
return res.json();
})
.then((data) => {
configCache = data;
return data;
})
.finally(() => {
configPromise = null;
});
return configPromise;
}
/**
* Hook to fetch runtime configuration
*
* Fetches app configuration from /api/config endpoint, which reads
* environment variables at runtime (not build time).
*
* The config is cached after first fetch to avoid unnecessary requests.
*/
export function useConfig(): AppConfig {
const [config, setConfig] = useState<AppConfig>({
appName: configCache?.appName || 'Webmail',
jmapServerUrl: configCache?.jmapServerUrl || '',
oauthEnabled: configCache?.oauthEnabled || false,
oauthClientId: configCache?.oauthClientId || '',
oauthIssuerUrl: configCache?.oauthIssuerUrl || '',
rememberMeEnabled: configCache?.rememberMeEnabled || false,
settingsSyncEnabled: configCache?.settingsSyncEnabled || false,
stalwartFeaturesEnabled: configCache?.stalwartFeaturesEnabled ?? true,
devMode: configCache?.devMode || false,
isLoading: !configCache,
error: null,
});
useEffect(() => {
// If already cached, no need to fetch
if (configCache) {
setConfig({
appName: configCache.appName,
jmapServerUrl: configCache.jmapServerUrl,
oauthEnabled: configCache.oauthEnabled,
oauthClientId: configCache.oauthClientId,
oauthIssuerUrl: configCache.oauthIssuerUrl,
rememberMeEnabled: configCache.rememberMeEnabled,
settingsSyncEnabled: configCache.settingsSyncEnabled,
stalwartFeaturesEnabled: configCache.stalwartFeaturesEnabled,
devMode: configCache.devMode,
isLoading: false,
error: null,
});
return;
}
fetchConfig()
.then((data) => {
setConfig({
appName: data.appName,
jmapServerUrl: data.jmapServerUrl,
oauthEnabled: data.oauthEnabled,
oauthClientId: data.oauthClientId,
oauthIssuerUrl: data.oauthIssuerUrl,
rememberMeEnabled: data.rememberMeEnabled,
settingsSyncEnabled: data.settingsSyncEnabled,
stalwartFeaturesEnabled: data.stalwartFeaturesEnabled,
devMode: data.devMode,
isLoading: false,
error: null,
});
})
.catch((err) => {
setConfig((prev) => ({
...prev,
isLoading: false,
error: err.message,
}));
});
}, []);
return config;
}