mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-07 00:02:07 +00:00
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1250 lines
33 KiB
TypeScript
1250 lines
33 KiB
TypeScript
// Models for /admin/* endpoints. Trimmed to what the admin app
|
|
// currently renders — broaden as we wire more pages.
|
|
|
|
export type WorkerInstallState =
|
|
| "pending"
|
|
| "provisioning"
|
|
| "installed"
|
|
| "error"
|
|
| "uninstalling"
|
|
| "uninstalled";
|
|
|
|
export type WorkerHealthState =
|
|
| "healthy"
|
|
| "watch"
|
|
| "throttled"
|
|
| "quarantined"
|
|
| "blocked";
|
|
|
|
export interface ManagedWorker {
|
|
id: string;
|
|
name: string;
|
|
notes: string;
|
|
ip_addr: string;
|
|
active: boolean;
|
|
account_count: number;
|
|
/** Sign-in geography hint the placer scores on. Empty is fine. */
|
|
region: string;
|
|
health_state: WorkerHealthState;
|
|
load_score: number;
|
|
|
|
ssh_host?: string;
|
|
ssh_port?: number;
|
|
ssh_user?: string;
|
|
ssh_public_key?: string;
|
|
ssh_host_fingerprint?: string;
|
|
install_state: WorkerInstallState;
|
|
last_seen_at?: string;
|
|
last_error?: string;
|
|
|
|
profile_id?: string;
|
|
config_applied_at?: string;
|
|
image_version?: string;
|
|
tags?: string[];
|
|
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
// A mailbox assigned to a worker, with its per-mailbox health signals, returned
|
|
// by GET /admin/workers/:id/emails.
|
|
export interface AdminWorkerEmail {
|
|
id: string;
|
|
email: string;
|
|
user_id: string;
|
|
organization_id?: string | null;
|
|
status: string;
|
|
provider: string;
|
|
warmup_enabled: boolean;
|
|
last_synced_at: string | null; // null until the mailbox syncs for the first time
|
|
risk_band: string; // clean | risky | quarantine
|
|
risk_evaluated_at?: string | null;
|
|
warmup_health?: string; // worst warmup health_state, "" if not in a pool
|
|
blocked_until?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AdminWorkerEmailsResult {
|
|
data: AdminWorkerEmail[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface CreateWorkerInput {
|
|
name: string;
|
|
notes?: string;
|
|
/** Optional sign-in geography hint. The only placement input an operator sets. */
|
|
region?: string;
|
|
ssh_host: string;
|
|
ssh_port?: number;
|
|
ssh_user?: string;
|
|
generate_enrollment_token?: boolean;
|
|
}
|
|
|
|
// The public key and enrollment token are returned once, at creation. Neither is
|
|
// retrievable later, so the create flow has to surface both before navigating.
|
|
export interface CreateWorkerResponse extends ManagedWorker {
|
|
ssh_public_key: string;
|
|
enrollment_token?: string;
|
|
enrollment_token_ttl_seconds?: number;
|
|
}
|
|
|
|
export interface WorkerLiveStatus {
|
|
service_active: boolean;
|
|
container_up: boolean;
|
|
container_image: string;
|
|
uptime: string;
|
|
raw: string;
|
|
}
|
|
|
|
export interface AdminAuditLog {
|
|
id: string;
|
|
admin_user_id: string;
|
|
action: string;
|
|
target_type: string;
|
|
target_id: string;
|
|
details?: Record<string, unknown>;
|
|
ip_address: string;
|
|
user_agent: string;
|
|
created_at: string;
|
|
admin_user?: {
|
|
id: string;
|
|
email: string;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
};
|
|
}
|
|
|
|
export interface AdminAuditLogSearch {
|
|
admin_user_id?: string;
|
|
action?: string;
|
|
target_type?: string;
|
|
target_id?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
cursor?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
export interface AdminAuditLogsResult {
|
|
// Nullable on purpose, like AdminUserPreview's slices: a backend older than
|
|
// the make-not-declare fix in pg_admin.go answers null for an empty page,
|
|
// and typing it as an array is what let `data.data.length` crash the page.
|
|
data: AdminAuditLog[] | null;
|
|
pagination: {
|
|
next_cursor?: string | null;
|
|
has_more?: boolean;
|
|
};
|
|
}
|
|
|
|
// /admin/analytics/overview — fields the backend returns vary, so keep
|
|
// the shape loose. The Overview page renders whatever counters come
|
|
// back and falls back to "—" for anything missing.
|
|
export interface PlatformOverview {
|
|
users_total?: number;
|
|
users_active_30d?: number;
|
|
organizations_total?: number;
|
|
workers_total?: number;
|
|
workers_active?: number;
|
|
workers_offline?: number;
|
|
mailboxes_total?: number;
|
|
mailboxes_connected?: number;
|
|
emails_sent_24h?: number;
|
|
emails_sent_7d?: number;
|
|
bounce_rate_7d?: number;
|
|
complaint_rate_7d?: number;
|
|
campaigns_running?: number;
|
|
[k: string]: unknown;
|
|
}
|
|
|
|
// /admin/mailboxes — cross-org mailbox admin.
|
|
|
|
export interface AdminMailboxRow {
|
|
id: string;
|
|
email: string;
|
|
provider: string;
|
|
status: string;
|
|
user_id: string;
|
|
owner_email: string;
|
|
organization_id?: string | null;
|
|
org_name?: string | null;
|
|
worker_id?: string | null;
|
|
warmup_enabled: boolean;
|
|
risk_band: string;
|
|
warmup_pool_type?: string | null;
|
|
campaign_limit: number;
|
|
last_synced_at?: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AdminMailboxesResult {
|
|
data: AdminMailboxRow[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminMailboxSearch {
|
|
q?: string;
|
|
status?: string;
|
|
provider?: string;
|
|
warmup?: "on" | "off" | "";
|
|
created_within?: number;
|
|
org_id?: string;
|
|
// Ownership / placement
|
|
user_id?: string;
|
|
worker_id?: string;
|
|
// Classification
|
|
risk_band?: string;
|
|
warmup_pool_type?: string;
|
|
synced_status?: "never" | "stale" | "recent" | "";
|
|
// Flags
|
|
warmup_paused?: boolean;
|
|
tracking_domain_verified?: boolean;
|
|
has_tracking_domain?: boolean;
|
|
has_organization?: boolean;
|
|
signature_sync?: boolean;
|
|
has_oauth?: boolean;
|
|
has_smtp_imap?: boolean;
|
|
// Numeric ranges
|
|
campaign_limit_min?: number;
|
|
campaign_limit_max?: number;
|
|
min_wait_time_min?: number;
|
|
min_wait_time_max?: number;
|
|
// Date ranges (YYYY-MM-DD)
|
|
created_after?: string;
|
|
created_before?: string;
|
|
last_synced_after?: string;
|
|
last_synced_before?: string;
|
|
cursor?: string;
|
|
limit?: number;
|
|
sort_by?: "email" | "created_at" | "last_synced_at" | "campaign_limit";
|
|
sort_desc?: boolean;
|
|
}
|
|
|
|
// /admin/analytics — time-series counters for the platform.
|
|
|
|
export interface DailyEmailStat {
|
|
date: string;
|
|
total_sent: number;
|
|
total_delivered: number;
|
|
total_bounced: number;
|
|
total_opened: number;
|
|
total_clicked: number;
|
|
total_replied: number;
|
|
}
|
|
|
|
export interface HourlyEmailStat {
|
|
hour: number;
|
|
total_sent: number;
|
|
}
|
|
|
|
export interface UserGrowthStat {
|
|
date: string;
|
|
new_users: number;
|
|
total_users: number;
|
|
}
|
|
|
|
export interface AnalyticsTrends {
|
|
users_growth_percent: number;
|
|
emails_growth_percent: number;
|
|
campaigns_growth_percent: number;
|
|
revenue_growth_percent: number;
|
|
}
|
|
|
|
// /admin/outreach — platform-mailer composer (send from noreply@warmbly.com
|
|
// with configurable Reply-To, every send audit-logged).
|
|
|
|
export type AdminOutreachStatus = "queued" | "sent" | "failed";
|
|
|
|
export interface AdminOutreachMessage {
|
|
id: string;
|
|
sent_by: string;
|
|
to_user_id?: string | null;
|
|
to_org_id?: string | null;
|
|
to_email: string;
|
|
reply_to?: string | null;
|
|
subject: string;
|
|
body: string;
|
|
status: AdminOutreachStatus;
|
|
error?: string | null;
|
|
sent_at?: string | null;
|
|
created_at: string;
|
|
sent_by_user?: AdminUserSummary;
|
|
to_user?: AdminUserSummary;
|
|
}
|
|
|
|
export interface SendAdminOutreachRequest {
|
|
to_user_id?: string;
|
|
to_org_id?: string;
|
|
to_email?: string;
|
|
reply_to?: string;
|
|
subject: string;
|
|
body: string;
|
|
}
|
|
|
|
export interface AdminOutreachSearch {
|
|
q?: string;
|
|
status?: AdminOutreachStatus | "";
|
|
recipient_type?: "user" | "org" | "email" | "";
|
|
sent_by_q?: string;
|
|
has_reply_to?: boolean;
|
|
has_error?: boolean;
|
|
has_user?: boolean;
|
|
has_org?: boolean;
|
|
// Date ranges
|
|
created_within?: number; // days; omit for any
|
|
created_after?: string; // YYYY-MM-DD
|
|
created_before?: string; // YYYY-MM-DD
|
|
sent_at_after?: string; // YYYY-MM-DD
|
|
sent_at_before?: string; // YYYY-MM-DD
|
|
cursor?: string;
|
|
limit?: number;
|
|
sort_by?: "created_at" | "sent_at" | "status" | "to_email" | "subject";
|
|
sort_desc?: boolean;
|
|
}
|
|
|
|
export interface AdminOutreachResult {
|
|
data: AdminOutreachMessage[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
// /admin/limit-requests — limit-increase request queue.
|
|
|
|
export type LimitRequestStatus =
|
|
| "pending"
|
|
| "approved"
|
|
| "rejected"
|
|
| "cancelled";
|
|
|
|
export interface LimitIncreaseRequest {
|
|
id: string;
|
|
organization_id: string;
|
|
field: string;
|
|
current_effective: number;
|
|
requested: number;
|
|
reason: string;
|
|
status: LimitRequestStatus;
|
|
submitted_by: string;
|
|
submitted_at: string;
|
|
reviewed_by?: string | null;
|
|
reviewed_at?: string | null;
|
|
review_notes: string;
|
|
organization?: {
|
|
id: string;
|
|
name: string;
|
|
slug?: string | null;
|
|
};
|
|
submitted_by_user?: {
|
|
id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
};
|
|
}
|
|
|
|
export interface AdminLimitRequestsResult {
|
|
data: LimitIncreaseRequest[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminLimitRequestSearch {
|
|
q?: string;
|
|
status?: LimitRequestStatus | "all" | "";
|
|
field?: string; // one of the 6 app-validated field keys, or "" for any
|
|
org_id?: string;
|
|
submitted_by?: string;
|
|
// Flags
|
|
reviewed?: boolean;
|
|
unreviewed?: boolean;
|
|
// Numeric ranges
|
|
requested_min?: number;
|
|
requested_max?: number;
|
|
current_effective_min?: number;
|
|
current_effective_max?: number;
|
|
// Date ranges
|
|
submitted_within?: number; // days; omit for any
|
|
submitted_after?: string; // YYYY-MM-DD
|
|
submitted_before?: string; // YYYY-MM-DD
|
|
reviewed_after?: string; // YYYY-MM-DD
|
|
reviewed_before?: string; // YYYY-MM-DD
|
|
cursor?: string;
|
|
limit?: number;
|
|
sort_by?:
|
|
| "submitted_at"
|
|
| "requested"
|
|
| "current_effective"
|
|
| "reviewed_at"
|
|
| "status"
|
|
| "field"
|
|
| "org_name";
|
|
sort_desc?: boolean;
|
|
}
|
|
|
|
// /admin/campaigns/* — platform-wide campaign admin (force-stop runaway
|
|
// campaigns, inspect engagement counters per campaign).
|
|
|
|
export interface AdminCampaignDetail {
|
|
id: string;
|
|
name: string;
|
|
user_id: string;
|
|
organization_id: string;
|
|
status: string;
|
|
created_at: string;
|
|
started_at?: string | null;
|
|
stopped_at?: string | null;
|
|
total_contacts: number;
|
|
emails_sent: number;
|
|
emails_opened: number;
|
|
emails_clicked: number;
|
|
emails_replied: number;
|
|
emails_bounced: number;
|
|
user?: AdminUserSummary;
|
|
organization?: {
|
|
id: string;
|
|
name: string;
|
|
slug?: string | null;
|
|
};
|
|
}
|
|
|
|
export interface AdminCampaignsResult {
|
|
data: AdminCampaignDetail[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminCampaignSearch {
|
|
q?: string;
|
|
user_id?: string;
|
|
org_id?: string;
|
|
status?: string; // "" | draft | active | paused | completed | paused_trial_expired | paused_no_accounts | paused_guardrail | paused_undeliverable
|
|
// Boolean flags
|
|
open_tracking?: boolean;
|
|
link_tracking?: boolean;
|
|
stop_on_reply?: boolean;
|
|
text_only?: boolean;
|
|
unsubscribe_header?: boolean;
|
|
// Relationship existence
|
|
has_contacts?: boolean;
|
|
has_bounces?: boolean;
|
|
// Count ranges
|
|
daily_limit_min?: number;
|
|
daily_limit_max?: number;
|
|
contact_count_min?: number;
|
|
contact_count_max?: number;
|
|
sent_count_min?: number;
|
|
sent_count_max?: number;
|
|
// Date ranges
|
|
created_within?: number;
|
|
created_after?: string;
|
|
created_before?: string;
|
|
start_date_after?: string;
|
|
start_date_before?: string;
|
|
updated_after?: string;
|
|
updated_before?: string;
|
|
// Pagination + sort
|
|
cursor?: string;
|
|
limit?: number;
|
|
sort_by?:
|
|
| "created_at"
|
|
| "name"
|
|
| "status"
|
|
| "updated_at"
|
|
| "daily_limit"
|
|
| "owner_email"
|
|
| "contact_count"
|
|
| "sent_count";
|
|
sort_desc?: boolean;
|
|
}
|
|
|
|
// /admin/warmup/* — warmup pool admin (the platform's most critical
|
|
// safety surface — keeps shared paid pools clean by quarantining
|
|
// risky mailboxes early).
|
|
|
|
export type WarmupAppealStatus = "pending" | "approved" | "rejected";
|
|
|
|
export interface WarmupPoolHealthSummary {
|
|
total_participants: number;
|
|
by_state: Record<string, number>;
|
|
avg_health_score: number;
|
|
avg_spam_placement_rate: number;
|
|
spam_placement_by_provider: Record<string, number>;
|
|
blocked_count: number;
|
|
at_risk_count: number;
|
|
}
|
|
|
|
export interface WarmupPoolInfo {
|
|
type: string;
|
|
total_participants: number;
|
|
active_participants: number;
|
|
blocked_count: number;
|
|
}
|
|
|
|
export interface AdminBlockedAccount {
|
|
id: string;
|
|
email: string;
|
|
user_id: string;
|
|
blocked_at: string;
|
|
blocked_by?: string | null;
|
|
block_reason: string;
|
|
has_appeal: boolean;
|
|
appeal_status?: WarmupAppealStatus | null;
|
|
user?: AdminUserSummary;
|
|
}
|
|
|
|
export interface WarmupAppeal {
|
|
id: string;
|
|
email_account_id: string;
|
|
user_id: string;
|
|
reason: string;
|
|
status: WarmupAppealStatus;
|
|
reviewed_by?: string | null;
|
|
reviewed_at?: string | null;
|
|
review_notes?: string | null;
|
|
created_at: string;
|
|
user?: AdminUserSummary;
|
|
email_account?: {
|
|
id: string;
|
|
email: string;
|
|
};
|
|
reviewed_by_user?: AdminUserSummary;
|
|
}
|
|
|
|
export interface WarmupAppealsResult {
|
|
data: WarmupAppeal[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminBlockedAccountsResult {
|
|
data: AdminBlockedAccount[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface BlockAccountRequest {
|
|
reason: string;
|
|
}
|
|
|
|
export interface ReviewAppealRequest {
|
|
approved: boolean;
|
|
notes: string;
|
|
}
|
|
|
|
// /admin/users/* — platform user admin.
|
|
|
|
export interface AdminUserSummary {
|
|
id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
}
|
|
|
|
export interface AdminUserDetail {
|
|
id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
max_organizations: number;
|
|
free_trial_used: boolean;
|
|
admin_permissions: number;
|
|
admin_granted_at?: string | null;
|
|
admin_granted_by?: string | null;
|
|
banned_at?: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
organization_count: number;
|
|
email_account_count: number;
|
|
campaign_count: number;
|
|
}
|
|
|
|
export interface AdminUsersResult {
|
|
data: AdminUserDetail[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminUserSearchParams {
|
|
q?: string;
|
|
status?: "active" | "banned" | "all" | "";
|
|
is_admin?: boolean;
|
|
has_overrides?: boolean;
|
|
free_trial_used?: boolean;
|
|
created_within?: number;
|
|
// Plan / subscription
|
|
plan_id?: string;
|
|
subscription_status?: string;
|
|
is_enterprise?: boolean;
|
|
has_subscription?: boolean;
|
|
has_active_subscription?: boolean;
|
|
// Account state
|
|
onboarding_completed?: boolean;
|
|
deletion_scheduled?: boolean;
|
|
has_avatar?: boolean;
|
|
has_active_campaign?: boolean;
|
|
has_ban_record?: boolean;
|
|
has_dedicated_worker?: boolean;
|
|
// Count ranges
|
|
org_count_min?: number;
|
|
org_count_max?: number;
|
|
email_account_count_min?: number;
|
|
email_account_count_max?: number;
|
|
campaign_count_min?: number;
|
|
campaign_count_max?: number;
|
|
max_organizations_min?: number;
|
|
max_organizations_max?: number;
|
|
// Date ranges (YYYY-MM-DD)
|
|
created_after?: string;
|
|
created_before?: string;
|
|
admin_granted_after?: string;
|
|
admin_granted_before?: string;
|
|
banned_after?: string;
|
|
banned_before?: string;
|
|
updated_after?: string;
|
|
updated_before?: string;
|
|
cursor?: string;
|
|
limit?: number;
|
|
sort_by?: "created_at" | "email" | "name";
|
|
sort_desc?: boolean;
|
|
}
|
|
|
|
export interface UserBan {
|
|
id: string;
|
|
user_id: string;
|
|
banned_by: string;
|
|
reason: string;
|
|
banned_at: string;
|
|
unbanned_at?: string | null;
|
|
unbanned_by?: string | null;
|
|
unban_reason?: string | null;
|
|
banned_by_user?: AdminUserSummary;
|
|
unbanned_by_user?: AdminUserSummary | null;
|
|
}
|
|
|
|
// One user's row in user_rate_limits: the API and realtime throughput they are
|
|
// allowed. Every value is set; a user with no row reads back the defaults.
|
|
export interface AdminUserRateLimits {
|
|
user_id: string;
|
|
limit_read_pm: number;
|
|
limit_write_pm: number;
|
|
limit_bulk_pm: number;
|
|
limit_unibox_pm: number;
|
|
limit_analytics_pm: number;
|
|
limit_api_calls_daily: number;
|
|
limit_bulk_ops_daily: number;
|
|
limit_ws_message_pm: number;
|
|
limit_ws_join_pm: number;
|
|
limit_ws_event_pm: number;
|
|
max_connections: number;
|
|
notes?: string | null;
|
|
updated_by?: string | null;
|
|
updated_at?: string | null; // absent when these are the defaults
|
|
}
|
|
|
|
export interface UpdateUserRateLimitsRequest {
|
|
limit_read_pm?: number;
|
|
limit_write_pm?: number;
|
|
limit_bulk_pm?: number;
|
|
limit_unibox_pm?: number;
|
|
limit_analytics_pm?: number;
|
|
limit_api_calls_daily?: number;
|
|
limit_bulk_ops_daily?: number;
|
|
limit_ws_message_pm?: number;
|
|
limit_ws_join_pm?: number;
|
|
limit_ws_event_pm?: number;
|
|
max_connections?: number;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface BanUserRequest {
|
|
reason: string;
|
|
// Bitmask of ban-scope flags. 1 = login, 2 = org-create, 4 = send.
|
|
// 0 falls back to "login only" server-side. See
|
|
// internal/models/admin.go BanScope.
|
|
scope?: number;
|
|
}
|
|
|
|
export interface UnbanUserRequest {
|
|
reason: string;
|
|
}
|
|
|
|
// The four slices are nullable on purpose: a backend older than the
|
|
// empty-slice fix in pg_admin.go answers null, and typing them as arrays is
|
|
// what let `email_accounts.length` take the whole page down.
|
|
export interface AdminUserPreview {
|
|
user: AdminUserDetail;
|
|
organizations: Array<{
|
|
id: string;
|
|
name: string;
|
|
slug?: string | null;
|
|
owner_user_id: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}> | null;
|
|
subscriptions: Array<{
|
|
id: string;
|
|
organization_id: string;
|
|
plan_id: string;
|
|
status: string;
|
|
is_enterprise: boolean;
|
|
current_period_end?: string | null;
|
|
trial_end?: string | null;
|
|
}> | null;
|
|
email_accounts: Array<{
|
|
id: string;
|
|
email: string;
|
|
organization_id?: string | null;
|
|
status: string;
|
|
provider: string;
|
|
warmup_enabled: boolean;
|
|
last_synced_at: string;
|
|
}> | null;
|
|
recent_bans: UserBan[] | null;
|
|
rate_limits?: AdminUserRateLimits | null;
|
|
}
|
|
|
|
// /admin/organizations* — workspace admin (read-only slice).
|
|
|
|
export interface AdminOrgListItem {
|
|
category: "standard" | "test";
|
|
id: string;
|
|
name: string;
|
|
slug?: string | null;
|
|
owner_user_id: string;
|
|
owner_email: string;
|
|
owner_first_name: string;
|
|
owner_last_name: string;
|
|
owner_banned_at?: string | null;
|
|
created_at: string;
|
|
deletion_scheduled_for?: string | null;
|
|
member_count: number;
|
|
email_account_count: number;
|
|
campaign_count: number;
|
|
active_campaigns: number;
|
|
risk_state?: OrgRiskState | null;
|
|
/** Acquisition channel recorded once at signup. Absent for a direct
|
|
* signup, which is most of them, and always absent on a self-host. */
|
|
utm_source?: string | null;
|
|
utm_medium?: string | null;
|
|
utm_campaign?: string | null;
|
|
landing_path?: string | null;
|
|
plan_name?: string | null;
|
|
plan_public?: boolean | null;
|
|
is_enterprise: boolean;
|
|
/** Paid because an operator said so, not because Stripe says so. */
|
|
managed_plan: boolean;
|
|
/** A grant that lapsed. Distinct from never having had one: the workspace
|
|
* is back on free and the reason is still on file. */
|
|
managed_plan_expired: boolean;
|
|
managed_plan_reason?: string | null;
|
|
managed_plan_until?: string | null;
|
|
}
|
|
|
|
/** The workspace's fused abuse posture. */
|
|
export type OrgRiskState = "trusted" | "watch" | "restricted" | "suspended";
|
|
|
|
/** One detector's finding. `expires_at` is absent when only a sweep or an
|
|
* operator can retract it. */
|
|
export interface OrgRiskSignal {
|
|
weight?: number;
|
|
detail?: string;
|
|
expires_at?: string;
|
|
}
|
|
|
|
/** An operator's pin over the derived posture. */
|
|
export interface OrgRiskOverride {
|
|
state: OrgRiskState;
|
|
reason?: string;
|
|
by?: string | null;
|
|
at?: string | null;
|
|
}
|
|
|
|
/** The operator view of a workspace's posture, evidence included. */
|
|
export interface OrgRisk {
|
|
organization_id: string;
|
|
state: OrgRiskState;
|
|
score: number;
|
|
reason?: string;
|
|
signals?: Record<string, OrgRiskSignal> | null;
|
|
evaluated_at?: string | null;
|
|
override?: OrgRiskOverride | null;
|
|
}
|
|
|
|
export interface SetOrgRiskOverrideRequest {
|
|
state: OrgRiskState;
|
|
reason: string;
|
|
}
|
|
|
|
export interface OrganizationLimits {
|
|
max_campaigns?: number | null;
|
|
max_active_campaigns?: number | null;
|
|
max_team_members?: number | null;
|
|
max_email_accounts?: number | null;
|
|
max_contacts?: number | null;
|
|
daily_campaign_limit?: number | null;
|
|
}
|
|
|
|
export interface OrganizationCounts {
|
|
total_campaigns: number;
|
|
active_campaigns: number;
|
|
total_contacts: number;
|
|
total_members: number;
|
|
email_accounts: number;
|
|
emails_sent_today: number;
|
|
}
|
|
|
|
export interface OrganizationLimitOverrides {
|
|
organization_id: string;
|
|
max_campaigns: number;
|
|
max_active_campaigns: number;
|
|
max_team_members: number;
|
|
max_email_accounts: number;
|
|
max_contacts: number;
|
|
daily_campaign_limit: number;
|
|
granted_by?: string | null;
|
|
granted_at: string;
|
|
updated_at: string;
|
|
notes: string;
|
|
}
|
|
|
|
export interface UpdateOrgOverridesRequest {
|
|
max_campaigns?: number;
|
|
max_active_campaigns?: number;
|
|
max_team_members?: number;
|
|
max_email_accounts?: number;
|
|
max_contacts?: number;
|
|
daily_campaign_limit?: number;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface AdminOrgDetail extends AdminOrgListItem {
|
|
updated_at: string;
|
|
deletion_scheduled_at?: string | null;
|
|
limits?: OrganizationLimits | null;
|
|
overrides?: OrganizationLimitOverrides | null;
|
|
effective_limits?: OrganizationLimits | null;
|
|
counts?: OrganizationCounts | null;
|
|
plan_name?: string | null;
|
|
subscription_status?: string | null;
|
|
is_enterprise: boolean;
|
|
current_period_end?: string | null;
|
|
trial_end?: string | null;
|
|
}
|
|
|
|
export interface AdminOrgsResult {
|
|
data: AdminOrgListItem[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminOrgMember {
|
|
id: string;
|
|
organization_id: string;
|
|
user_id: string;
|
|
role: string;
|
|
permissions: number;
|
|
invited_by?: string | null;
|
|
invited_at: string;
|
|
accepted_at?: string | null;
|
|
user?: {
|
|
id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
};
|
|
}
|
|
|
|
export interface AdminOrgMembersResult {
|
|
data: AdminOrgMember[];
|
|
}
|
|
|
|
export interface AdminOrgSearch {
|
|
q?: string;
|
|
status?: "active" | "pending_deletion" | "";
|
|
plan_id?: string;
|
|
plan_visibility?: "public" | "private" | "none" | "";
|
|
created_within?: number; // days; omit for any
|
|
has_overrides?: boolean;
|
|
enterprise?: boolean;
|
|
/** Plans an operator granted rather than Stripe. */
|
|
managed_plan?: boolean;
|
|
risk_state?: OrgRiskState | "";
|
|
risk_flagged?: boolean;
|
|
// Acquisition channel
|
|
utm_source?: string;
|
|
utm_medium?: string;
|
|
utm_campaign?: string;
|
|
has_acquisition?: boolean;
|
|
no_acquisition?: boolean;
|
|
// Subscription state
|
|
subscription_status?: string;
|
|
cancel_at_period_end?: boolean;
|
|
has_active_subscription?: boolean;
|
|
no_subscription?: boolean;
|
|
owner_banned?: boolean;
|
|
// Relationship existence
|
|
has_active_campaigns?: boolean;
|
|
has_email_accounts?: boolean;
|
|
// Count ranges
|
|
member_count_min?: number;
|
|
member_count_max?: number;
|
|
email_account_count_min?: number;
|
|
email_account_count_max?: number;
|
|
campaign_count_min?: number;
|
|
campaign_count_max?: number;
|
|
// Date ranges (YYYY-MM-DD)
|
|
created_after?: string;
|
|
created_before?: string;
|
|
trial_end_after?: string;
|
|
trial_end_before?: string;
|
|
current_period_end_after?: string;
|
|
current_period_end_before?: string;
|
|
updated_after?: string;
|
|
updated_before?: string;
|
|
cursor?: string;
|
|
limit?: number;
|
|
sort_by?: "created_at" | "name" | "owner_email" | "member_count" | "email_account_count" | "campaign_count";
|
|
sort_desc?: boolean;
|
|
}
|
|
|
|
/** A plan an operator granted rather than Stripe, for internal workspaces,
|
|
* design partners and support gestures. */
|
|
export interface ManagedPlan {
|
|
managed: boolean;
|
|
expired: boolean;
|
|
plan_id: string;
|
|
granted_at?: string | null;
|
|
granted_by?: string | null;
|
|
reason?: string | null;
|
|
until?: string | null;
|
|
}
|
|
|
|
/** A plan row as the admin plans endpoint returns it. Only the fields the
|
|
* grant picker needs. */
|
|
export interface AdminPlan {
|
|
id: string;
|
|
name?: string | null;
|
|
public?: boolean | null;
|
|
price?: number | string | null;
|
|
}
|
|
|
|
/** An account excused from the emailed login code. */
|
|
export interface LoginCodeExemption {
|
|
user_id: string;
|
|
email: string;
|
|
reason?: string | null;
|
|
granted_at?: string | null;
|
|
/** When the handed-out password stops working; absent for an account
|
|
* exempted without one. */
|
|
password_expires_at?: string | null;
|
|
}
|
|
|
|
/** The one time the password is readable. It is not stored in a form anyone
|
|
* can recover, so losing it means making another tester. */
|
|
export interface CreatedTester {
|
|
user_id: string;
|
|
email: string;
|
|
organization_id: string;
|
|
password: string;
|
|
/** When the password stops working. */
|
|
password_expires_at: string;
|
|
/** True when the tester joined a workspace that already existed rather
|
|
* than one minted for it. */
|
|
joined_existing: boolean;
|
|
}
|
|
|
|
/** A workspace role, as the Testers page offers them. Roles are ordinary rows
|
|
* an org can rename or delete, so the list is per workspace and not fixed. */
|
|
export interface AdminOrgRole {
|
|
id: string;
|
|
organization_id: string;
|
|
name: string;
|
|
description?: string | null;
|
|
color?: string | null;
|
|
permissions: number;
|
|
}
|
|
|
|
// --- Promo codes -----------------------------------------------------------
|
|
//
|
|
// Mirrors internal/models/discount.go. A code is the operator-owned half of
|
|
// the discount system: the customer types it at checkout, the backend
|
|
// validates it here and mints a matching one-off Stripe coupon per redemption.
|
|
|
|
export type DiscountType = "percent" | "fixed" | "trial_extension";
|
|
export type DiscountDuration = "once" | "repeating" | "forever";
|
|
export type DiscountCodeStatus = "active" | "disabled" | "expired";
|
|
export type DiscountRedemptionStatus = "pending" | "applied" | "canceled";
|
|
|
|
export interface DiscountCode {
|
|
id: string;
|
|
code: string;
|
|
description: string;
|
|
type: DiscountType;
|
|
|
|
percent_off?: number | null;
|
|
amount_off?: number | null;
|
|
currency?: string | null;
|
|
trial_extension_days?: number | null;
|
|
|
|
duration: DiscountDuration;
|
|
duration_in_months?: number | null;
|
|
|
|
/** null = uncapped across all workspaces. */
|
|
max_redemptions?: number | null;
|
|
times_redeemed: number;
|
|
/** How many times ONE workspace may redeem it. Always at least 1. */
|
|
per_account_limit: number;
|
|
|
|
applies_to_all_plans: boolean;
|
|
plan_ids: string[];
|
|
|
|
status: DiscountCodeStatus;
|
|
starts_at?: string | null;
|
|
expires_at?: string | null;
|
|
|
|
created_by?: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
/** One workspace's use of a code. The value fields are a snapshot taken at
|
|
* redemption, so editing the code later never rewrites history. */
|
|
export interface DiscountRedemption {
|
|
id: string;
|
|
discount_code_id: string;
|
|
organization_id: string;
|
|
redeemed_by?: string | null;
|
|
subscription_id?: string | null;
|
|
plan_id?: string | null;
|
|
stripe_coupon_id?: string | null;
|
|
stripe_checkout_session_id?: string | null;
|
|
|
|
type: DiscountType;
|
|
percent_off?: number | null;
|
|
amount_off?: number | null;
|
|
currency?: string | null;
|
|
trial_extension_days?: number | null;
|
|
|
|
status: DiscountRedemptionStatus;
|
|
redeemed_at: string;
|
|
applied_at?: string | null;
|
|
code?: string;
|
|
}
|
|
|
|
export interface AdminDiscountsResult {
|
|
data: DiscountCode[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminDiscountRedemptionsResult {
|
|
data: DiscountRedemption[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminDiscountSearch {
|
|
/** The backend binds free text as `search`, not `q`. */
|
|
search?: string;
|
|
status?: DiscountCodeStatus | "all" | "";
|
|
type?: DiscountType | "";
|
|
duration?: DiscountDuration | "";
|
|
|
|
plan_scope?: "all" | "specific" | "";
|
|
plan_id?: string;
|
|
|
|
has_redemptions?: boolean;
|
|
has_max_redemptions?: boolean;
|
|
exhausted?: boolean;
|
|
has_expiry?: boolean;
|
|
|
|
times_redeemed_min?: number;
|
|
times_redeemed_max?: number;
|
|
percent_off_min?: number;
|
|
percent_off_max?: number;
|
|
|
|
created_within?: number;
|
|
created_after?: string;
|
|
created_before?: string;
|
|
starts_after?: string;
|
|
starts_before?: string;
|
|
expires_after?: string;
|
|
expires_before?: string;
|
|
|
|
cursor?: string;
|
|
limit?: number;
|
|
sort_by?:
|
|
| "created_at"
|
|
| "code"
|
|
| "status"
|
|
| "times_redeemed"
|
|
| "expires_at"
|
|
| "starts_at"
|
|
| "updated_at";
|
|
sort_desc?: boolean;
|
|
}
|
|
|
|
export interface CreateDiscountCodeRequest {
|
|
code: string;
|
|
description?: string;
|
|
type: DiscountType;
|
|
percent_off?: number;
|
|
amount_off?: number;
|
|
currency?: string;
|
|
trial_extension_days?: number;
|
|
duration?: DiscountDuration;
|
|
duration_in_months?: number;
|
|
max_redemptions?: number;
|
|
per_account_limit?: number;
|
|
applies_to_all_plans: boolean;
|
|
plan_ids?: string[];
|
|
status?: DiscountCodeStatus;
|
|
starts_at?: string;
|
|
expires_at?: string;
|
|
}
|
|
|
|
/** Partial update. `type` is immutable; recreate the code to change kinds.
|
|
* The three caps take an explicit null to clear them: an omitted key leaves
|
|
* the column alone, which is not the same as lifting an expiry. */
|
|
export type UpdateDiscountCodeRequest = Partial<
|
|
Omit<CreateDiscountCodeRequest, "code" | "type" | "max_redemptions" | "starts_at" | "expires_at">
|
|
> & {
|
|
max_redemptions?: number | null;
|
|
starts_at?: string | null;
|
|
expires_at?: string | null;
|
|
};
|
|
|
|
// Community app directory (/admin/app-listings). published = link only (listed
|
|
// once enough workspaces use it), featured = picked by us, hidden = taken down.
|
|
export type AppListingStatus = "published" | "featured" | "hidden";
|
|
|
|
export interface AdminAppListing {
|
|
application_id: string;
|
|
organization_id: string;
|
|
slug: string;
|
|
tagline: string;
|
|
description: string;
|
|
category: string;
|
|
install_url: string;
|
|
support_url: string;
|
|
privacy_url: string;
|
|
status: AppListingStatus;
|
|
status_note?: string;
|
|
status_at?: string | null;
|
|
submitted_at: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
name: string;
|
|
logo_url: string;
|
|
website_url: string;
|
|
scopes: number;
|
|
permissions: { name: string; value: number; description: string; category: string }[];
|
|
app_status: string;
|
|
organization_name: string;
|
|
installs: number;
|
|
/** Shown in the directory: featured, or used by enough workspaces. */
|
|
listed: boolean;
|
|
status_by?: string | null;
|
|
status_by_email?: string;
|
|
}
|
|
|
|
export interface AdminAppListingsResult {
|
|
data: AdminAppListing[];
|
|
pagination: {
|
|
total?: number | null;
|
|
next_cursor?: string | null;
|
|
has_more: boolean;
|
|
};
|
|
}
|
|
|
|
export interface AdminAppListingSearch {
|
|
q?: string;
|
|
status?: AppListingStatus | "";
|
|
limit?: number;
|
|
cursor?: string;
|
|
}
|
|
|
|
// OAuth app moderation (/admin/oauth-apps) and developer blocks.
|
|
export interface AdminOAuthApp {
|
|
id: string;
|
|
organization_id: string;
|
|
organization_name: string;
|
|
created_by: string;
|
|
created_by_email: string;
|
|
name: string;
|
|
description: string;
|
|
logo_url: string;
|
|
website_url: string;
|
|
client_id: string;
|
|
redirect_uris: string[];
|
|
webhook_url: string;
|
|
permissions: { name: string; value: number; description: string; category: string }[];
|
|
status: "active" | "disabled";
|
|
is_public: boolean;
|
|
suspended_at?: string | null;
|
|
suspended_reason?: string;
|
|
installs: number;
|
|
listing_slug?: string;
|
|
listing_status?: AppListingStatus | "";
|
|
org_blocked: boolean;
|
|
creator_blocked: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AdminOAuthAppsResult {
|
|
data: AdminOAuthApp[];
|
|
pagination: { total?: number | null; next_cursor?: string | null; has_more: boolean };
|
|
}
|
|
|
|
export interface AdminOAuthAppSearch {
|
|
q?: string;
|
|
status?: "active" | "disabled" | "suspended" | "";
|
|
limit?: number;
|
|
cursor?: string;
|
|
}
|
|
|
|
export interface OAuthDeveloperBlock {
|
|
id: string;
|
|
organization_id?: string | null;
|
|
organization_name?: string;
|
|
user_id?: string | null;
|
|
user_email?: string;
|
|
reason: string;
|
|
blocked_by_email?: string;
|
|
created_at: string;
|
|
}
|