intermittent commit

This commit is contained in:
2026-07-14 13:22:10 +02:00
parent 8aa1943581
commit e6f7c45f0a
76 changed files with 6039 additions and 2188 deletions

View File

@@ -0,0 +1,56 @@
import type { ApiSettings } from "../types";
import { apiFetch, apiGetList } from "./client";
export type PermissionItem = {
scope: string;
label: string;
description: string;
category: string;
level: "tenant" | "system";
system_template_id?: string | null;
system_required?: boolean;
};
export type AdminOverview = {
active_tenant_id: string;
active_tenant_name: string;
tenant_count?: number | null;
system_account_count?: number | null;
system_group_template_count?: number | null;
system_role_template_count?: number | null;
user_count: number;
active_user_count: number;
group_count: number;
role_count: number;
active_api_key_count: number;
capabilities: string[];
};
export type TenantAdminItem = {
id: string;
slug: string;
name: string;
description?: string | null;
default_locale: string;
settings: Record<string, unknown>;
allow_custom_groups?: boolean | null;
allow_custom_roles?: boolean | null;
allow_api_keys?: boolean | null;
effective_governance: Record<string, boolean>;
is_active: boolean;
counts: Record<string, number>;
created_at: string;
updated_at: string;
};
export function fetchAdminOverview(settings: ApiSettings): Promise<AdminOverview> {
return apiFetch(settings, "/api/v1/admin/overview");
}
export async function fetchPermissionCatalog(settings: ApiSettings): Promise<PermissionItem[]> {
return apiGetList<PermissionItem, "permissions">(settings, "/api/v1/admin/permissions", "permissions");
}
export async function fetchTenants(settings: ApiSettings): Promise<TenantAdminItem[]> {
return apiGetList<TenantAdminItem, "tenants">(settings, "/api/v1/admin/tenants", "tenants");
}

View File

@@ -1,4 +1,4 @@
import type { ApiSettings, AuthInfo, LoginResponse, UserUiPreferences } from "../types";
import type { ApiSettings, AuthGroupsInfo, AuthInfo, AuthProfileInfo, AuthRolesInfo, AuthSessionInfo, AuthShellInfo, LoginResponse, UserUiPreferences } from "../types";
import { apiFetch } from "./client";
export async function login(
@@ -15,8 +15,28 @@ export async function fetchMe(settings: ApiSettings): Promise<AuthInfo> {
return apiFetch<AuthInfo>(settings, "/api/v1/auth/me");
}
export async function switchTenant(settings: ApiSettings, tenantId: string): Promise<AuthInfo> {
return apiFetch<AuthInfo>(settings, "/api/v1/auth/switch-tenant", {
export async function fetchSession(settings: ApiSettings): Promise<AuthSessionInfo> {
return apiFetch<AuthSessionInfo>(settings, "/api/v1/auth/session", { cache: "no-store" });
}
export async function fetchShellAuth(settings: ApiSettings): Promise<AuthShellInfo> {
return apiFetch<AuthShellInfo>(settings, "/api/v1/auth/shell", { cache: "no-store" });
}
export async function fetchAuthProfile(settings: ApiSettings): Promise<AuthProfileInfo> {
return apiFetch<AuthProfileInfo>(settings, "/api/v1/auth/profile", { cache: "no-store" });
}
export async function fetchAuthRoles(settings: ApiSettings): Promise<AuthRolesInfo> {
return apiFetch<AuthRolesInfo>(settings, "/api/v1/auth/roles", { cache: "no-store" });
}
export async function fetchAuthGroups(settings: ApiSettings): Promise<AuthGroupsInfo> {
return apiFetch<AuthGroupsInfo>(settings, "/api/v1/auth/groups", { cache: "no-store" });
}
export async function switchTenant(settings: ApiSettings, tenantId: string): Promise<AuthShellInfo> {
return apiFetch<AuthShellInfo>(settings, "/api/v1/auth/switch-tenant", {
method: "POST",
body: JSON.stringify({ tenant_id: tenantId })
});
@@ -35,8 +55,8 @@ export async function updateProfile(
enabled_language_codes?: string[] | null;
ui_preferences?: Partial<UserUiPreferences> | null;
}
): Promise<AuthInfo> {
return apiFetch<AuthInfo>(settings, "/api/v1/auth/profile", {
): Promise<AuthProfileInfo> {
return apiFetch<AuthProfileInfo>(settings, "/api/v1/auth/profile", {
method: "PATCH",
body: JSON.stringify(payload)
});

View File

@@ -1,9 +1,9 @@
import type { ApiSettings } from "../types";
const STORAGE_KEY = "multimailer.apiSettings";
const LEGACY_STORAGE_KEYS = ["i18n:govoplan-core.multimailer_apisettings.1d1601d4"];
const SESSION_STORAGE_KEY = "multimailer.session";
const CSRF_COOKIE_NAME = import.meta.env.VITE_CSRF_COOKIE_NAME ?? "msm_csrf";
const STORAGE_KEY = "govoplan.apiSettings";
const LEGACY_STORAGE_KEYS: string[] = [];
const SESSION_STORAGE_KEY = "govoplan.session";
const CSRF_COOKIE_NAME = import.meta.env.VITE_CSRF_COOKIE_NAME ?? "govoplan_csrf";
const RECENT_SAFE_REQUEST_TTL_MS = 750;
const MAX_RECENT_SAFE_REQUESTS = 100;
const MAX_CONDITIONAL_SAFE_REQUESTS = 200;
@@ -77,6 +77,56 @@ export function apiUrl(settings: ApiSettings, path: string): string {
return baseUrl ? `${baseUrl}${normalizedPath}` : normalizedPath;
}
export type ApiQueryValue = string | number | boolean | null | undefined;
export type ApiQueryParams = Record<string, ApiQueryValue | readonly ApiQueryValue[]>;
function queryValue(value: ApiQueryValue): string | null {
if (value === null || value === undefined || value === "") return null;
return String(value);
}
export function apiQuery(params: ApiQueryParams = {}): string {
const search = new URLSearchParams();
for (const [key, rawValue] of Object.entries(params)) {
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
for (const value of values) {
const normalized = queryValue(value);
if (normalized !== null) search.append(key, normalized);
}
}
const query = search.toString();
return query ? `?${query}` : "";
}
export function apiPath(path: string, params: ApiQueryParams = {}): string {
const query = apiQuery(params);
if (!query) return path;
return path.includes("?") ? `${path}&${query.slice(1)}` : `${path}${query}`;
}
export async function apiGetList<TItem, K extends string>(
settings: ApiSettings,
path: string,
key: K,
params: ApiQueryParams = {}
): Promise<TItem[]> {
const response = await apiFetch<Record<K, TItem[] | null | undefined>>(settings, apiPath(path, params));
return response[key] ?? [];
}
export function apiPost<TResponse>(settings: ApiSettings, path: string, init: Omit<RequestInit, "method"> = {}): Promise<TResponse> {
return apiFetch<TResponse>(settings, path, { ...init, method: "POST" });
}
export function apiPostJson<TResponse, TPayload = unknown>(
settings: ApiSettings,
path: string,
payload: TPayload,
init: Omit<RequestInit, "method" | "body"> = {}
): Promise<TResponse> {
return apiPost<TResponse>(settings, path, { ...init, body: JSON.stringify(payload) });
}
export function loadApiSettings(): ApiSettings {
const storedBaseUrl = loadStoredSetting("baseUrl");
const storedApiKey = sessionStorage.getItem(`${SESSION_STORAGE_KEY}.apiKey`);

View File

@@ -0,0 +1,143 @@
import type {
MailImapTransportSettings,
MailProfilePatternKey,
MailProfilePolicy,
MailProfileScope,
MailSecurity,
MailServerProfile,
MailServerProfileCredentials,
MailTransportCredentials,
MailTransportSettings
} from "../types";
export type {
MailCredentialPolicy,
MailProfilePatternKey,
MailProfilePolicy,
MailProfileScope,
MailSecurity,
MailServerProfile
} from "../types";
export type MailSmtpTestPayload = MailTransportSettings;
export type MailImapTestPayload = MailImapTransportSettings;
export type MailTransportCredentialsPayload = MailTransportCredentials;
export type MailServerProfileCredentialsPayload = MailServerProfileCredentials;
export type MailServerProfileListResponse = { profiles: MailServerProfile[] };
export type MailProfilePatternRules = Partial<Record<MailProfilePatternKey, string[]>>;
export type MailConnectionTestResponse = {
ok: boolean;
protocol: "smtp" | "imap";
host?: string | null;
port?: number | null;
security?: MailSecurity | string | null;
message: string;
details?: Record<string, unknown>;
};
export type MailImapFolderResponse = {
name: string;
flags?: string[];
message_count?: number | null;
unseen_count?: number | null;
};
export type MailImapFolderListResponse = {
ok: boolean;
protocol: "imap";
host?: string | null;
port?: number | null;
security?: MailSecurity | string | null;
message: string;
folders: MailImapFolderResponse[];
detected_sent_folder?: string | null;
from_cache?: boolean;
refreshing?: boolean;
indexed_at?: string | null;
details?: Record<string, unknown>;
};
export const mailProfilePatternKeys = [
"smtp_hosts",
"imap_hosts",
"envelope_senders",
"from_headers",
"recipient_domains"
] as const satisfies readonly MailProfilePatternKey[];
export const mailProfilePolicyLimitKeys = [
"allowed_profile_ids",
"allow_user_profiles",
"allow_group_profiles",
"smtp_credentials.inherit",
"imap_credentials.inherit",
"whitelist.smtp_hosts",
"whitelist.imap_hosts",
"whitelist.envelope_senders",
"whitelist.from_headers",
"whitelist.recipient_domains",
"blacklist.smtp_hosts",
"blacklist.imap_hosts",
"blacklist.envelope_senders",
"blacklist.from_headers",
"blacklist.recipient_domains"
] as const;
export type MailProfilePolicyLimitKey = typeof mailProfilePolicyLimitKeys[number];
export type MailProfilePolicyLimitPermissions = Partial<Record<MailProfilePolicyLimitKey, boolean>>;
export type MailPolicySourceStep = {
scope_type: string;
scope_id?: string | null;
label: string;
applied_fields?: string[];
policy?: MailProfilePolicy | null;
};
export type MailProfilePolicyResponse = {
scope_type: MailProfileScope;
scope_id?: string | null;
policy: MailProfilePolicy;
effective_policy?: MailProfilePolicy | null;
parent_policy?: MailProfilePolicy | null;
effective_policy_sources?: MailPolicySourceStep[];
parent_policy_sources?: MailPolicySourceStep[];
};
export type MailServerProfilePayload = {
name: string;
slug?: string | null;
description?: string | null;
is_active?: boolean;
scope_type?: MailProfileScope;
scope_id?: string | null;
smtp: MailSmtpTestPayload;
imap?: MailImapTestPayload | null;
credentials?: MailServerProfileCredentialsPayload | null;
};
export type MockMailboxMessage = {
id: string;
kind: "smtp" | "imap_append" | string;
created_at: string;
envelope_from?: string | null;
envelope_recipients?: string[];
subject?: string | null;
from_header?: string | null;
to_header?: string | null;
cc_header?: string | null;
bcc_header?: string | null;
message_id?: string | null;
size_bytes?: number;
body_preview?: string | null;
attachment_count?: number;
folder?: string | null;
raw_eml?: string | null;
headers?: Record<string, string>;
attachments?: Array<{ filename?: string | null; content_type?: string | null; size_bytes?: number }>;
};
export type MockMailboxMessageResponse = {
message: MockMailboxMessage;
};

View File

@@ -1,5 +1,5 @@
import type { ApiSettings } from "../types";
import { apiFetch } from "./client";
import { apiFetch, apiPath } from "./client";
export type PrivacyRetentionPolicyFieldKey =
| "store_raw_campaign_json"
@@ -78,13 +78,6 @@ export type RetentionRunResponse = {
};
};
function retentionPolicyQuery(scopeId?: string | null): string {
const params = new URLSearchParams();
if (scopeId) params.set("scope_id", scopeId);
const suffix = params.toString();
return suffix ? `?${suffix}` : "";
}
export function getPrivacyRetentionPolicy(
settings: ApiSettings,
scope: PrivacyRetentionPolicyScope,
@@ -92,7 +85,7 @@ export function getPrivacyRetentionPolicy(
): Promise<PrivacyRetentionPolicyScopeResponse> {
return apiFetch(
settings,
`/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}${retentionPolicyQuery(scopeId)}`
apiPath(`/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}`, { scope_id: scopeId })
);
}
@@ -103,7 +96,7 @@ export function explainPrivacyRetentionPolicy(
): Promise<PrivacyRetentionPolicyExplainResponse> {
return apiFetch(
settings,
`/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}/explain${retentionPolicyQuery(scopeId)}`
apiPath(`/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}/explain`, { scope_id: scopeId })
);
}
@@ -116,7 +109,7 @@ export function updatePrivacyRetentionPolicy(
): Promise<PrivacyRetentionPolicyScopeResponse> {
return apiFetch(
settings,
`/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}${retentionPolicyQuery(scopeId)}`,
apiPath(`/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}`, { scope_id: scopeId }),
{ method: "PUT", body: JSON.stringify({ policy, change_request_id: changeRequestId ?? null }) }
);
}