intermittent commit
This commit is contained in:
56
webui/src/api/adminCommon.ts
Normal file
56
webui/src/api/adminCommon.ts
Normal 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");
|
||||
}
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -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`);
|
||||
|
||||
143
webui/src/api/mailContracts.ts
Normal file
143
webui/src/api/mailContracts.ts
Normal 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;
|
||||
};
|
||||
@@ -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 }) }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user