Files
Aegis/frontend/src/api/settings.ts
kitos 69d92f500a
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
feat(tempo): per-user Tempo API token — same pattern as Jira token
Each user can now store their own personal Tempo API token in their
profile settings. Time is logged using each user's own credentials.

Backend:
- Migration b044: adds tempo_api_token column to users table
- User model: adds tempo_api_token column
- UserPreferencesUpdate: adds tempo_api_token field (write-only)
- UserOut: adds tempo_api_token (excluded) + tempo_token_set bool;
  @model_validator derives both jira_token_set and tempo_token_set
- users router: handles tempo_api_token same as jira_api_token
  (empty string clears it, never returned in responses)
- tempo_service: refactored to per-user token; has_tempo_configured(),
  get_user_tempo_client(user) use user.tempo_api_token; global
  TEMPO_ENABLED still acts as kill-switch
- system router: /system/tempo-test now uses current user's personal
  token (any role); removed global TEMPO_API_TOKEN dependency

Frontend:
- settings.ts: UserPreferencesUpdate.tempo_api_token, UserMeOut.tempo_token_set
- SettingsPage ProfileSection: Tempo Integration section with password
  field, show/hide toggle, configured badge, and Test Tempo button —
  mirrors the Jira token UX exactly
- JiraConfigSection: removed stale global Tempo test block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 10:46:38 +02:00

203 lines
5.6 KiB
TypeScript

import client from "./client";
// ---------------------------------------------------------------------------
// Email / SMTP config (admin only)
// ---------------------------------------------------------------------------
export interface EmailConfigOut {
enabled: boolean;
host: string;
port: number;
username: string;
from_email: string;
use_tls: boolean;
}
export interface EmailConfigUpdate {
enabled?: boolean;
host?: string;
port?: number;
username?: string;
password?: string;
from_email?: string;
use_tls?: boolean;
}
export async function getEmailConfig(): Promise<EmailConfigOut> {
const { data } = await client.get<EmailConfigOut>("/system/email-config");
return data;
}
export async function updateEmailConfig(payload: EmailConfigUpdate): Promise<EmailConfigOut> {
const { data } = await client.patch<EmailConfigOut>("/system/email-config", payload);
return data;
}
export async function sendTestEmail(to: string): Promise<{ detail: string }> {
const { data } = await client.post<{ detail: string }>("/system/email-test", { to });
return data;
}
// ---------------------------------------------------------------------------
// Webhook config (admin + leads)
// ---------------------------------------------------------------------------
export interface WebhookOut {
id: string;
name: string;
url: string;
secret: string | null;
events: string[];
is_active: boolean;
created_at: string | null;
last_triggered_at: string | null;
failure_count: number;
}
export interface WebhookCreate {
name: string;
url: string;
secret?: string;
events: string[];
is_active?: boolean;
}
export interface WebhookUpdate {
name?: string;
url?: string;
secret?: string;
events?: string[];
is_active?: boolean;
}
export async function getWebhooks(): Promise<WebhookOut[]> {
const { data } = await client.get<WebhookOut[]>("/webhooks");
return data;
}
export async function createWebhook(payload: WebhookCreate): Promise<WebhookOut> {
const { data } = await client.post<WebhookOut>("/webhooks", payload);
return data;
}
export async function updateWebhook(id: string, payload: WebhookUpdate): Promise<WebhookOut> {
const { data } = await client.patch<WebhookOut>(`/webhooks/${id}`, payload);
return data;
}
export async function deleteWebhook(id: string): Promise<void> {
await client.delete(`/webhooks/${id}`);
}
export async function testWebhook(id: string): Promise<{ detail: string }> {
const { data } = await client.post<{ detail: string }>(`/webhooks/${id}/test`);
return data;
}
// ---------------------------------------------------------------------------
// User preferences (all users)
// ---------------------------------------------------------------------------
export interface NotificationPreferences {
// Universal
email_on_test_validated: boolean;
email_on_test_rejected: boolean;
email_on_campaign_completed: boolean;
email_on_new_mitre_techniques: boolean;
email_on_stale_coverage: boolean;
in_app_all: boolean;
// Tech + leads
email_on_assigned_to_campaign?: boolean;
email_on_test_state_change?: boolean;
// Leads only
email_on_all_team_validations?: boolean;
email_on_webhook_failures?: boolean;
// Admin only
email_on_new_users?: boolean;
email_on_system_errors?: boolean;
}
export interface UserPreferencesUpdate {
notification_preferences?: Partial<NotificationPreferences>;
jira_account_id?: string | null;
jira_api_token?: string | null;
/** Atlassian email used for Jira auth. Overrides Aegis account email. Empty string clears it. */
jira_email?: string | null;
/** Personal Tempo API token. Empty string clears it. */
tempo_api_token?: string | null;
}
export interface UserMeOut {
id: string;
username: string;
email: string | null;
role: string;
is_active: boolean;
must_change_password: boolean;
created_at: string | null;
last_login: string | null;
notification_preferences: NotificationPreferences | null;
jira_account_id: string | null;
jira_email: string | null;
jira_token_set: boolean;
tempo_token_set: boolean;
}
export async function getMe(): Promise<UserMeOut> {
const { data } = await client.get<UserMeOut>("/users/me");
return data;
}
export async function updateMyPreferences(payload: UserPreferencesUpdate): Promise<UserMeOut> {
const { data } = await client.patch<UserMeOut>("/users/me/preferences", payload);
return data;
}
// ---------------------------------------------------------------------------
// Jira system config (admin only)
// ---------------------------------------------------------------------------
export interface JiraConfigOut {
enabled: boolean;
url: string;
project_key: string;
parent_ticket: string;
}
export interface JiraConfigUpdate {
enabled?: boolean;
url?: string;
project_key?: string;
parent_ticket?: string;
}
export async function getJiraConfig(): Promise<JiraConfigOut> {
const { data } = await client.get<JiraConfigOut>("/system/jira-config");
return data;
}
export async function updateJiraConfig(payload: JiraConfigUpdate): Promise<JiraConfigOut> {
const { data } = await client.patch<JiraConfigOut>("/system/jira-config", payload);
return data;
}
export async function testJiraConnection(): Promise<{
status: "ok" | "error";
connected_as?: string;
jira_url?: string;
message?: string;
}> {
const { data } = await client.post("/system/jira-test");
return data;
}
export async function testTempoConnection(): Promise<{
status: "ok" | "error" | "disabled";
message?: string;
worklogs_found?: number | string;
}> {
const { data } = await client.post("/system/tempo-test");
return data;
}