first wokring prototype
This commit is contained in:
20
src/api/auth.ts
Normal file
20
src/api/auth.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ApiSettings, AuthInfo, LoginResponse } from "../types";
|
||||
import { apiFetch } from "./client";
|
||||
|
||||
export async function login(
|
||||
settings: ApiSettings,
|
||||
payload: { email: string; password: string }
|
||||
): Promise<LoginResponse> {
|
||||
return apiFetch<LoginResponse>({ ...settings, accessToken: "", apiKey: "" }, "/api/v1/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchMe(settings: ApiSettings): Promise<AuthInfo> {
|
||||
return apiFetch<AuthInfo>(settings, "/api/v1/auth/me");
|
||||
}
|
||||
|
||||
export async function logout(settings: ApiSettings): Promise<void> {
|
||||
await apiFetch(settings, "/api/v1/auth/logout", { method: "POST" });
|
||||
}
|
||||
289
src/api/campaigns.ts
Normal file
289
src/api/campaigns.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import type { ApiSettings, CampaignListItem } from "../types";
|
||||
import { apiFetch } from "./client";
|
||||
|
||||
export type CampaignListResponse =
|
||||
| CampaignListItem[]
|
||||
| {
|
||||
campaigns?: CampaignListItem[];
|
||||
items?: CampaignListItem[];
|
||||
results?: CampaignListItem[];
|
||||
};
|
||||
|
||||
export type CampaignCreateMinimalPayload = {
|
||||
external_id?: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
current_flow?: string;
|
||||
current_step?: string;
|
||||
};
|
||||
|
||||
export type CampaignCreateResponse = {
|
||||
campaign: CampaignListItem & {
|
||||
current_version_id?: string | null;
|
||||
};
|
||||
version: CampaignVersionListItem;
|
||||
};
|
||||
|
||||
export type CampaignVersionListItem = {
|
||||
id: string;
|
||||
campaign_id: string;
|
||||
version_number: number;
|
||||
schema_version?: string;
|
||||
source_filename?: string | null;
|
||||
source_base_path?: string | null;
|
||||
workflow_state?: string;
|
||||
current_flow?: string;
|
||||
current_step?: string | null;
|
||||
is_complete?: boolean;
|
||||
editor_state?: Record<string, unknown>;
|
||||
autosaved_at?: string | null;
|
||||
published_at?: string | null;
|
||||
locked_at?: string | null;
|
||||
locked_by_user_id?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
validation_summary?: Record<string, unknown> | null;
|
||||
build_summary?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type CampaignVersionDetail = CampaignVersionListItem & {
|
||||
raw_json: Record<string, unknown>;
|
||||
campaign_json?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CampaignVersionUpdatePayload = {
|
||||
campaign_json?: Record<string, unknown> | null;
|
||||
current_flow?: string | null;
|
||||
current_step?: string | null;
|
||||
workflow_state?: string | null;
|
||||
is_complete?: boolean | null;
|
||||
editor_state?: Record<string, unknown> | null;
|
||||
source_filename?: string | null;
|
||||
source_base_path?: string | null;
|
||||
};
|
||||
|
||||
export type CampaignPartialValidationPayload = {
|
||||
campaign_json?: Record<string, unknown> | null;
|
||||
section?: string | null;
|
||||
};
|
||||
|
||||
export type CampaignPartialValidationResponse = {
|
||||
ok: boolean;
|
||||
section?: string | null;
|
||||
error_count: number;
|
||||
warning_count: number;
|
||||
info_count: number;
|
||||
issues: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
export type CampaignSummary = {
|
||||
generated_at?: string;
|
||||
campaign?: CampaignListItem;
|
||||
current_version?: {
|
||||
id: string;
|
||||
version_number?: number;
|
||||
schema_version?: string;
|
||||
source_filename?: string | null;
|
||||
created_at?: string | null;
|
||||
validation_summary?: Record<string, unknown> | null;
|
||||
build_summary?: Record<string, unknown> | null;
|
||||
} | null;
|
||||
cards?: {
|
||||
jobs_total?: number;
|
||||
queueable?: number;
|
||||
needs_attention?: number;
|
||||
sent?: number;
|
||||
failed?: number;
|
||||
imap_appended?: number;
|
||||
imap_failed?: number;
|
||||
};
|
||||
status_counts?: Record<string, Record<string, number>>;
|
||||
issues?: Record<string, unknown>;
|
||||
attachments?: Record<string, unknown>;
|
||||
attempts?: Record<string, unknown>;
|
||||
delivery?: Record<string, unknown>;
|
||||
recent_failures?: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
export type CampaignQueuePayload = {
|
||||
version_id?: string | null;
|
||||
include_warnings?: boolean;
|
||||
enqueue_celery?: boolean;
|
||||
dry_run?: boolean;
|
||||
};
|
||||
|
||||
export async function listCampaigns(settings: ApiSettings): Promise<CampaignListItem[]> {
|
||||
const response = await apiFetch<CampaignListResponse>(settings, "/api/v1/campaigns");
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return response.campaigns ?? response.items ?? response.results ?? [];
|
||||
}
|
||||
|
||||
export async function getCampaign(settings: ApiSettings, campaignId: string): Promise<CampaignListItem> {
|
||||
return apiFetch<CampaignListItem>(settings, `/api/v1/campaigns/${campaignId}`);
|
||||
}
|
||||
|
||||
export async function createNewCampaign(
|
||||
settings: ApiSettings,
|
||||
overrides: CampaignCreateMinimalPayload = {}
|
||||
): Promise<CampaignCreateResponse> {
|
||||
const now = new Date();
|
||||
const stamp = now.toISOString().slice(0, 19).replace(/[-:T]/g, "");
|
||||
const payload = {
|
||||
external_id: overrides.external_id ?? `new-campaign-${stamp}`,
|
||||
name: overrides.name ?? "New Campaign",
|
||||
description: overrides.description ?? "",
|
||||
current_flow: overrides.current_flow ?? "create",
|
||||
current_step: overrides.current_step ?? "basics"
|
||||
};
|
||||
|
||||
return apiFetch<CampaignCreateResponse>(settings, "/api/v1/campaigns/new", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCampaignSchema(settings: ApiSettings): Promise<unknown> {
|
||||
return apiFetch(settings, "/api/v1/schemas/campaign");
|
||||
}
|
||||
|
||||
export async function listCampaignVersions(
|
||||
settings: ApiSettings,
|
||||
campaignId: string
|
||||
): Promise<CampaignVersionListItem[]> {
|
||||
return apiFetch<CampaignVersionListItem[]>(settings, `/api/v1/campaigns/${campaignId}/versions`);
|
||||
}
|
||||
|
||||
export async function getCampaignVersion(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
versionId: string
|
||||
): Promise<CampaignVersionDetail> {
|
||||
return apiFetch<CampaignVersionDetail>(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}`);
|
||||
}
|
||||
|
||||
export async function updateCampaignVersion(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
versionId: string,
|
||||
payload: CampaignVersionUpdatePayload
|
||||
): Promise<CampaignVersionDetail> {
|
||||
return apiFetch<CampaignVersionDetail>(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function autosaveCampaignVersion(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
versionId: string,
|
||||
payload: CampaignVersionUpdatePayload
|
||||
): Promise<CampaignVersionDetail> {
|
||||
return apiFetch<CampaignVersionDetail>(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}/autosave`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function setCampaignVersionStep(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
versionId: string,
|
||||
currentStep: string,
|
||||
currentFlow?: string | null
|
||||
): Promise<CampaignVersionDetail> {
|
||||
return apiFetch<CampaignVersionDetail>(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}/set-step`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ current_flow: currentFlow, current_step: currentStep })
|
||||
});
|
||||
}
|
||||
|
||||
export async function validatePartial(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
versionId: string,
|
||||
payload: CampaignPartialValidationPayload = {}
|
||||
): Promise<CampaignPartialValidationResponse> {
|
||||
return apiFetch<CampaignPartialValidationResponse>(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}/validate-partial`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function publishCampaignVersion(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
versionId: string
|
||||
): Promise<CampaignVersionDetail> {
|
||||
return apiFetch<CampaignVersionDetail>(settings, `/api/v1/campaigns/${campaignId}/versions/${versionId}/publish`, {
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
export async function validateVersion(
|
||||
settings: ApiSettings,
|
||||
versionId: string,
|
||||
checkFiles = false
|
||||
): Promise<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/versions/${versionId}/validate`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ check_files: checkFiles })
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildVersion(
|
||||
settings: ApiSettings,
|
||||
versionId: string,
|
||||
writeEml = true
|
||||
): Promise<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/versions/${versionId}/build`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ write_eml: writeEml })
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCampaignSummary(settings: ApiSettings, campaignId: string): Promise<CampaignSummary> {
|
||||
return apiFetch<CampaignSummary>(settings, `/api/v1/campaigns/${campaignId}/summary`);
|
||||
}
|
||||
|
||||
export async function getCampaignJobs(settings: ApiSettings, campaignId: string): Promise<{ jobs: Record<string, unknown>[] }> {
|
||||
return apiFetch<{ jobs: Record<string, unknown>[] }>(settings, `/api/v1/campaigns/${campaignId}/jobs`);
|
||||
}
|
||||
|
||||
export async function queueCampaign(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
payload: CampaignQueuePayload = {}
|
||||
): Promise<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/queue`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function pauseCampaign(settings: ApiSettings, campaignId: string): Promise<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/pause`, { method: "POST" });
|
||||
}
|
||||
|
||||
export async function resumeCampaign(settings: ApiSettings, campaignId: string): Promise<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/resume`, { method: "POST" });
|
||||
}
|
||||
|
||||
export async function cancelCampaign(settings: ApiSettings, campaignId: string): Promise<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/cancel`, { method: "POST" });
|
||||
}
|
||||
|
||||
export async function appendSent(
|
||||
settings: ApiSettings,
|
||||
campaignId: string,
|
||||
dryRun = false
|
||||
): Promise<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/append-sent`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ dry_run: dryRun })
|
||||
});
|
||||
}
|
||||
62
src/api/client.ts
Normal file
62
src/api/client.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { ApiSettings } from "../types";
|
||||
|
||||
const STORAGE_KEY = "multimailer.apiSettings";
|
||||
|
||||
export function loadApiSettings(): ApiSettings {
|
||||
const storedBaseUrl = localStorage.getItem(`${STORAGE_KEY}.baseUrl`);
|
||||
|
||||
return {
|
||||
// Empty base URL means "same origin". In Vite dev, /api is proxied to FastAPI.
|
||||
apiBaseUrl:
|
||||
storedBaseUrl !== null
|
||||
? storedBaseUrl
|
||||
: import.meta.env.VITE_API_BASE_URL ?? "",
|
||||
apiKey: localStorage.getItem(`${STORAGE_KEY}.apiKey`) || "",
|
||||
accessToken: localStorage.getItem(`${STORAGE_KEY}.accessToken`) || ""
|
||||
};
|
||||
}
|
||||
|
||||
export function saveApiSettings(settings: ApiSettings): void {
|
||||
localStorage.setItem(`${STORAGE_KEY}.baseUrl`, settings.apiBaseUrl);
|
||||
localStorage.setItem(`${STORAGE_KEY}.apiKey`, settings.apiKey);
|
||||
localStorage.setItem(`${STORAGE_KEY}.accessToken`, settings.accessToken);
|
||||
}
|
||||
|
||||
export function clearAccessToken(): void {
|
||||
localStorage.removeItem(`${STORAGE_KEY}.accessToken`);
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(settings: ApiSettings, path: string, init?: RequestInit): Promise<T> {
|
||||
const baseUrl = settings.apiBaseUrl.trim().replace(/\/$/, "");
|
||||
const url = baseUrl ? `${baseUrl}${path}` : path;
|
||||
|
||||
const headers = new Headers(init?.headers || {});
|
||||
|
||||
if (!(init?.body instanceof FormData) && !headers.has("Content-Type")) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
if (settings.accessToken) {
|
||||
headers.set("Authorization", `Bearer ${settings.accessToken}`);
|
||||
} else if (settings.apiKey) {
|
||||
headers.set("X-API-Key", settings.apiKey);
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...init, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`${response.status} ${response.statusText}: ${text}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
return (await response.text()) as T;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
75
src/api/mail.ts
Normal file
75
src/api/mail.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { ApiSettings } from "../types";
|
||||
import { apiFetch } from "./client";
|
||||
|
||||
export type MailSecurity = "plain" | "tls" | "starttls";
|
||||
|
||||
export type MailSmtpTestPayload = {
|
||||
host?: string | null;
|
||||
port?: number | null;
|
||||
username?: string | null;
|
||||
password?: string | null;
|
||||
security?: MailSecurity;
|
||||
timeout_seconds?: number;
|
||||
};
|
||||
|
||||
export type MailImapTestPayload = MailSmtpTestPayload & {
|
||||
enabled?: boolean;
|
||||
sent_folder?: string | null;
|
||||
};
|
||||
|
||||
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[];
|
||||
};
|
||||
|
||||
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;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export async function testSmtpSettings(
|
||||
settings: ApiSettings,
|
||||
payload: MailSmtpTestPayload
|
||||
): Promise<MailConnectionTestResponse> {
|
||||
return apiFetch<MailConnectionTestResponse>(settings, "/api/v1/mail/test-smtp", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function testImapSettings(
|
||||
settings: ApiSettings,
|
||||
payload: MailImapTestPayload
|
||||
): Promise<MailConnectionTestResponse> {
|
||||
return apiFetch<MailConnectionTestResponse>(settings, "/api/v1/mail/test-imap", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function listImapFolders(
|
||||
settings: ApiSettings,
|
||||
payload: MailImapTestPayload
|
||||
): Promise<MailImapFolderListResponse> {
|
||||
return apiFetch<MailImapFolderListResponse>(settings, "/api/v1/mail/list-imap-folders", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user