Files
gatehouse-ui/src/lib/api.ts
T

2320 lines
81 KiB
TypeScript
Raw Normal View History

2026-03-06 00:22:57 +05:45
// API Client for Secuird Backend
2026-01-07 14:29:40 +00:00
// Uses Bearer token authentication
2026-01-06 15:33:03 +00:00
2026-01-06 15:42:41 +00:00
import { config } from '@/config';
2026-01-06 15:33:03 +00:00
interface ApiResponse<T = unknown> {
version: string;
success: boolean;
code: number;
message: string;
data?: T;
error?: {
type: string;
details: Record<string, unknown>;
};
}
export interface User {
id: string;
email: string;
2026-01-06 16:06:53 +00:00
email_verified: boolean;
2026-01-06 15:33:03 +00:00
full_name: string | null;
avatar_url: string | null;
2026-01-06 16:06:53 +00:00
status: string;
last_login_at: string | null;
created_at: string;
updated_at: string;
2026-03-01 16:50:19 +05:45
// Fields present in admin list view
org_role?: string;
org_id?: string;
activated?: boolean;
2026-03-04 18:43:12 +05:45
// Auth method capabilities — present on /users/me response
has_password?: boolean;
totp_enabled?: boolean;
linked_providers?: string[];
2026-03-06 14:19:54 +05:45
/** Session-derived group memberships (from OIDC claims or session device_info). */
groups?: string[];
/** Whether the current user is allowed to access the system-wide audit log. */
can_view_system_logs?: boolean;
2026-01-06 16:06:53 +00:00
}
export interface Organization {
id: string;
name: string;
slug: string;
description: string | null;
logo_url: string | null;
2026-01-06 15:33:03 +00:00
is_active: boolean;
2026-01-06 16:06:53 +00:00
role: string;
2026-01-06 15:33:03 +00:00
created_at: string;
updated_at: string;
}
2026-01-06 16:06:53 +00:00
export interface OrganizationsResponse {
organizations: Organization[];
count: number;
}
2026-01-16 17:31:25 +10:30
export interface MfaComplianceOrgSummary {
organization_id: string;
organization_name: string;
status: string;
deadline_at: string | null;
effective_mode: string;
applied_at: string;
}
export interface MfaComplianceSummary {
overall_status: string;
missing_methods: string[];
deadline_at: string | null;
orgs: MfaComplianceOrgSummary[];
}
/**
* Check if MFA is required for the user based on their compliance status.
* This checks if any organization has an effective_mode that starts with "require_",
* which handles require_webauthn, require_totp, or any future MFA methods.
*/
export function isMfaRequired(compliance: MfaComplianceSummary | null): boolean {
if (!compliance || !compliance.orgs) return false;
return compliance.orgs.some(
org => org.effective_mode && org.effective_mode.startsWith('require_')
);
}
2026-01-06 15:33:03 +00:00
export interface LoginResponse {
2026-01-14 07:21:55 +00:00
user?: User;
token?: string;
expires_at?: string;
2026-01-12 06:28:36 +00:00
requires_totp?: boolean;
2026-01-16 17:50:56 +10:30
requires_webauthn?: boolean;
2026-01-16 17:31:25 +10:30
requires_mfa_enrollment?: boolean;
mfa_compliance?: MfaComplianceSummary;
/** Set on login when the user belongs to no organisations. */
requires_org_setup?: boolean;
/** Pending invitations for the user's email (present when requires_org_setup is true). */
pending_invites?: PendingInvite[];
/** True when the registering user is the very first user on this instance. */
is_first_user?: boolean;
2026-01-12 06:28:36 +00:00
}
export interface TotpEnrollResponse {
secret: string;
provisioning_uri: string;
qr_code: string; // base64 PNG
backup_codes: string[];
}
export interface TotpStatusResponse {
totp_enabled: boolean;
verified_at: string | null;
backup_codes_remaining: number;
}
export interface TotpVerifyResponse {
user: User;
token: string;
expires_at: string;
2026-01-06 15:33:03 +00:00
}
export interface ProfileResponse {
user: User;
}
2026-01-14 15:32:30 +00:00
// WebAuthn types
export interface PasskeyCredential {
id: string;
name: string;
transports: string[];
device_type: string;
created_at: string;
last_used_at: string | null;
}
export interface WebAuthnStatusResponse {
webauthn_enabled: boolean;
credential_count: number;
}
export interface WebAuthnCredentialsResponse {
credentials: PasskeyCredential[];
count: number;
}
export interface WebAuthnLoginCompleteResponse {
user: User;
token: string;
expires_at: string;
}
2026-03-04 18:43:12 +05:45
// Admin MFA management types
export interface AdminMfaMethod {
/** Unique identifier: auth_method.id for TOTP, credential id for WebAuthn */
id: string;
/** 'totp' or 'webauthn' */
type: 'totp' | 'webauthn';
/** Human-readable name */
name: string;
device_type?: string;
transports?: string[];
verified: boolean;
created_at: string | null;
last_used_at: string | null;
}
export interface AdminLinkedAccount {
/** UUID of the AuthenticationMethod row */
id: string;
/** Provider name: 'google' | 'github' | 'microsoft' | 'oidc' */
provider_type: string;
/** Email address from the OAuth provider, if available */
email: string | null;
/** Display name from the OAuth provider, if available */
name: string | null;
/** ISO timestamp when the account was linked */
linked_at: string | null;
}
export interface AdminUserSshCertificate {
id: string;
ca_id: string;
user_id: string;
ssh_key_id: string | null;
serial: string;
key_id: string;
cert_type: 'user' | 'host';
principals: string[];
valid_after: string;
valid_before: string;
revoked: boolean;
revoked_at: string | null;
revoke_reason: string | null;
status: 'issued' | 'revoked' | 'expired' | 'superseded';
request_ip: string | null;
request_user_agent: string | null;
critical_options: Record<string, string>;
extensions: Record<string, string>;
created_at: string;
updated_at: string;
is_valid: boolean;
days_until_expiry: number;
ssh_key: {
id: string;
fingerprint: string;
key_type: string;
key_bits: number;
key_comment: string | null;
description: string | null;
verified: boolean;
} | null;
}
2026-01-21 03:09:38 +10:30
// External Auth Types
export type ExternalProviderId = 'google' | 'github' | 'microsoft';
2026-01-20 15:54:11 +10:30
export interface ExternalProvider {
2026-01-21 03:09:38 +10:30
id: string;
2026-01-20 15:54:11 +10:30
name: string;
2026-01-21 03:09:38 +10:30
type: string;
is_configured: boolean;
2026-01-20 15:54:11 +10:30
is_active: boolean;
2026-01-21 03:09:38 +10:30
settings: {
requires_domain: boolean;
supports_refresh_tokens: boolean;
};
2026-01-20 15:54:11 +10:30
}
2026-01-21 03:09:38 +10:30
export interface ExternalProvidersResponse {
providers: ExternalProvider[];
2026-01-20 15:54:11 +10:30
}
export interface LinkedAccount {
id: string;
2026-01-21 03:09:38 +10:30
provider_type: string;
provider_user_id: string;
email: string | null;
name: string | null;
picture: string | null;
verified: boolean;
linked_at: string | null;
last_used_at: string | null;
}
export interface LinkedAccountsResponse {
linked_accounts: LinkedAccount[];
unlink_available: boolean;
2026-01-20 15:54:11 +10:30
}
export interface PrincipalOption {
id: string;
name: string;
description: string | null;
}
export interface MyPrincipalsOrg {
org_id: string;
org_name: string;
role: string;
is_admin: boolean;
my_principals: PrincipalOption[];
all_principals: PrincipalOption[]; // populated for admin/owner only
}
export interface MyPrincipalsResponse {
orgs: MyPrincipalsOrg[];
}
2026-01-20 15:54:11 +10:30
export interface OAuthAuthorizeResponse {
authorization_url: string;
state: string;
}
export interface OAuthCallbackResponse {
2026-01-21 03:09:38 +10:30
token: string;
expires_in: number;
token_type: string;
user: User;
}
export interface LinkAccountResponse {
linked_account: LinkedAccount;
2026-01-20 15:54:11 +10:30
}
2026-03-08 18:08:42 +05:45
export interface OrganizationApiKey {
id: string;
organization_id: string;
name: string;
description: string | null;
key_hash?: string; // Usually excluded from responses for security
last_used_at: string | null;
is_revoked: boolean;
revoked_at: string | null;
revoke_reason: string | null;
created_at: string;
updated_at: string;
}
export interface CertificateAuditLog {
id: string;
action: string;
certificate_serial: string;
key_id: string;
principals: string[];
user_id: string;
user_email: string | null;
issued_at: string;
valid_after: string;
valid_before: string;
ip_address: string | null;
user_agent: string | null;
message: string | null;
success: boolean;
created_at: string;
}
2026-01-06 15:33:03 +00:00
class ApiError extends Error {
code: number;
type: string;
details: Record<string, unknown>;
constructor(message: string, code: number, type: string, details: Record<string, unknown> = {}) {
super(message);
this.name = 'ApiError';
this.code = code;
this.type = type;
this.details = details;
}
}
2026-01-07 14:29:40 +00:00
// Token storage keys
2026-03-06 00:22:57 +05:45
const TOKEN_KEY = 'secuird_token';
const TOKEN_EXPIRY_KEY = 'secuird_token_expiry';
2026-01-07 14:29:40 +00:00
// Token management
export const tokenManager = {
getToken: (): string | null => {
const token = localStorage.getItem(TOKEN_KEY);
const expiry = localStorage.getItem(TOKEN_EXPIRY_KEY);
// Check if token is expired
if (token && expiry) {
const expiryDate = new Date(expiry);
if (expiryDate <= new Date()) {
tokenManager.clearToken();
return null;
}
}
return token;
},
2026-01-15 23:15:04 +00:00
setToken: (token: string, expiresAt?: string | null): void => {
2026-01-07 14:29:40 +00:00
localStorage.setItem(TOKEN_KEY, token);
2026-01-15 23:15:04 +00:00
if (expiresAt) {
localStorage.setItem(TOKEN_EXPIRY_KEY, expiresAt);
} else {
localStorage.removeItem(TOKEN_EXPIRY_KEY);
}
2026-01-07 14:29:40 +00:00
},
clearToken: (): void => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(TOKEN_EXPIRY_KEY);
},
hasValidToken: (): boolean => {
return tokenManager.getToken() !== null;
2026-01-07 14:29:40 +00:00
},
};
2026-01-14 02:10:23 +00:00
// Error types that indicate the session/token is truly invalid
const SESSION_INVALID_ERROR_TYPES = [
'INVALID_TOKEN',
'TOKEN_EXPIRED',
'SESSION_EXPIRED',
'AUTH_ERROR',
'UNAUTHORIZED',
];
2026-01-16 17:31:25 +10:30
export const AUTHORIZATION_ERROR_TYPES = ['AUTHORIZATION_ERROR'] as const;
2026-01-14 02:10:23 +00:00
interface RequestConfig {
// Controls token clearing on 401:
// - 'auto' (default): Clear only if error type indicates invalid session
// - true: Always clear token on 401
// - false: Never clear token on 401
clearTokenOn401?: boolean | 'auto';
2026-01-16 17:31:25 +10:30
// Optional callback for handling 403 authorization errors
on403?: (error: ApiError) => void;
2026-01-14 02:10:23 +00:00
}
2026-01-07 14:29:40 +00:00
// Central request function - all API calls go through here
2026-01-06 15:33:03 +00:00
async function request<T>(
endpoint: string,
2026-01-07 14:29:40 +00:00
options: RequestInit = {},
2026-01-14 02:10:23 +00:00
requiresAuth = true,
requestConfig: RequestConfig = {}
2026-01-06 15:33:03 +00:00
): Promise<T> {
2026-01-16 17:31:25 +10:30
const { clearTokenOn401 = 'auto', on403 } = requestConfig;
2026-01-14 02:10:23 +00:00
2026-01-07 14:29:40 +00:00
const headers: Record<string, string> = {
'Content-Type': 'application/json',
2026-01-12 01:24:59 +00:00
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
2026-01-07 14:29:40 +00:00
...(options.headers as Record<string, string>),
};
// Add Authorization header if we have a token and auth is required
if (requiresAuth) {
const token = tokenManager.getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
2026-01-06 15:42:41 +00:00
const response = await fetch(`${config.api.baseUrl}${endpoint}`, {
2026-01-06 15:33:03 +00:00
...options,
2026-01-07 14:29:40 +00:00
headers,
2026-01-15 22:46:27 +00:00
credentials: 'include', // Always include cookies for session consistency
2026-01-12 01:24:59 +00:00
cache: 'no-store',
2026-01-06 15:33:03 +00:00
});
const json: ApiResponse<T> = await response.json();
if (!json.success) {
2026-01-14 02:10:23 +00:00
const errorType = json.error?.type || 'UNKNOWN_ERROR';
// Handle 401 token clearing based on configuration
2026-01-07 14:29:40 +00:00
if (json.code === 401) {
2026-01-14 02:10:23 +00:00
const shouldClearToken =
clearTokenOn401 === true ||
(clearTokenOn401 === 'auto' && SESSION_INVALID_ERROR_TYPES.includes(errorType));
if (shouldClearToken) {
tokenManager.clearToken();
// Dispatch event so the UI can show a session timeout modal
window.dispatchEvent(new CustomEvent('session:expired'));
2026-01-14 02:10:23 +00:00
if (import.meta.env.DEV) {
console.log(`[API] Token cleared on 401 (type: ${errorType}, endpoint: ${endpoint})`);
}
} else if (import.meta.env.DEV) {
console.log(`[API] 401 received but token preserved (type: ${errorType}, endpoint: ${endpoint})`);
}
2026-01-07 14:29:40 +00:00
}
2026-01-16 17:31:25 +10:30
// Handle 403 authorization errors
if (json.code === 403) {
const error = new ApiError(
json.message || 'Access denied',
json.code,
errorType,
json.error?.details || {}
);
if (on403) {
on403(error);
}
throw error;
}
2026-01-07 14:29:40 +00:00
2026-01-06 15:33:03 +00:00
throw new ApiError(
json.message || 'An error occurred',
json.code,
2026-01-14 02:10:23 +00:00
errorType,
2026-01-06 15:33:03 +00:00
json.error?.details || {}
);
}
return json.data as T;
}
2026-01-07 14:29:40 +00:00
// Centralized API client - all routes defined here
2026-01-06 15:33:03 +00:00
export const api = {
auth: {
2026-01-07 14:29:40 +00:00
login: async (email: string, password: string, remember_me = false): Promise<LoginResponse> => {
const response = await request<LoginResponse>('/auth/login', {
2026-01-06 15:33:03 +00:00
method: 'POST',
body: JSON.stringify({ email, password, remember_me }),
2026-01-14 07:21:55 +00:00
credentials: 'include', // Required for TOTP session tracking
2026-01-07 14:29:40 +00:00
}, false); // Login doesn't require auth
2026-01-14 07:21:55 +00:00
// Only store token if login is complete (no TOTP required)
2026-01-15 23:15:04 +00:00
if (response.token && !response.requires_totp) {
tokenManager.setToken(response.token, response.expires_at ?? null);
2026-01-14 07:21:55 +00:00
}
2026-01-07 14:29:40 +00:00
return response;
},
2026-01-06 15:33:03 +00:00
register: async (email: string, password: string, full_name?: string): Promise<LoginResponse> => {
const response = await request<LoginResponse>('/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, password_confirm: password, full_name }),
}, false);
if (response.token) {
tokenManager.setToken(response.token, response.expires_at ?? null);
}
return response;
},
forgotPassword: (email: string): Promise<{ message: string }> =>
request<{ message: string }>('/auth/forgot-password', {
method: 'POST',
body: JSON.stringify({ email }),
}, false),
resetPassword: (token: string, password: string): Promise<{ message: string }> =>
request<{ message: string }>('/auth/reset-password', {
method: 'POST',
body: JSON.stringify({ token, password, password_confirm: password }),
}, false),
verifyEmail: (token: string): Promise<{ message: string }> =>
request<{ message: string }>('/auth/verify-email', {
method: 'POST',
body: JSON.stringify({ token }),
}, false),
resendVerification: (email: string): Promise<{ message: string }> =>
request<{ message: string }>('/auth/resend-verification', {
method: 'POST',
body: JSON.stringify({ email }),
}, false),
activate: (activation_key: string): Promise<{ message: string }> =>
request<{ message: string }>('/auth/activate', {
method: 'POST',
body: JSON.stringify({ activation_key }),
}, false),
resendActivation: (email: string): Promise<{ message: string }> =>
request<{ message: string }>('/auth/resend-activation', {
method: 'POST',
body: JSON.stringify({ email }),
}, false),
2026-01-07 14:29:40 +00:00
logout: async (): Promise<void> => {
try {
await request<void>('/auth/logout', {
method: 'POST',
});
} finally {
// Always clear token on logout
tokenManager.clearToken();
}
},
2026-01-06 15:33:03 +00:00
},
users: {
2026-01-14 02:10:23 +00:00
// me() is the canonical session validity check - always clear token on 401
me: () => request<ProfileResponse>('/users/me', {}, true, { clearTokenOn401: true }),
2026-01-06 15:33:03 +00:00
updateMe: (data: { full_name?: string; avatar_url?: string }) =>
request<ProfileResponse>('/users/me', {
method: 'PATCH',
body: JSON.stringify(data),
}),
2026-01-06 16:06:53 +00:00
// Delete the current user's own account (soft delete)
deleteMe: (requestConfig?: RequestConfig) =>
request<{ message: string }>('/users/me', { method: 'DELETE' }, true, requestConfig),
2026-01-16 17:31:25 +10:30
organizations: (requestConfig?: RequestConfig) =>
request<OrganizationsResponse>('/users/me/organizations', {}, true, requestConfig),
2026-01-11 08:17:15 +00:00
// Get the current user's effective principals across all orgs
myPrincipals: (requestConfig?: RequestConfig) =>
request<MyPrincipalsResponse>('/users/me/principals', {}, true, requestConfig),
2026-01-14 02:10:23 +00:00
// Password change can return 401 for wrong current password - don't clear token
2026-01-11 08:17:15 +00:00
changePassword: (currentPassword: string, newPassword: string, newPasswordConfirm: string) =>
request<{ message: string }>('/users/me/password', {
method: 'POST',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword,
new_password_confirm: newPasswordConfirm,
}),
2026-01-14 02:10:23 +00:00
}, true, { clearTokenOn401: false }),
// Get audit logs for the currently authenticated user
auditLogs: (params?: Record<string, string>, requestConfig?: RequestConfig) =>
request<{ audit_logs: AuditLogEntry[]; count: number; page: number; per_page: number; pages: number }>(
`/auth/audit-logs${params ? '?' + new URLSearchParams(params).toString() : ''}`,
{},
true,
requestConfig,
),
2026-03-01 16:50:19 +05:45
// Get pending (unaccepted) invitations for the logged-in user
getMyInvites: (requestConfig?: RequestConfig) =>
request<{ invites: PendingInvite[] }>('/users/me/invites', {}, true, requestConfig),
// Get the current user's department + principal memberships across all orgs
getMyMemberships: (requestConfig?: RequestConfig) =>
request<{ orgs: MyOrgMembership[] }>('/users/me/memberships', {}, true, requestConfig),
},
admin: {
// Get all system audit logs (admin view — returns all logs for org owners, own logs otherwise)
getAuditLogs: (params?: Record<string, string>, requestConfig?: RequestConfig) =>
request<{ audit_logs: AuditLogEntry[]; count: number; page: number; per_page: number; pages: number; is_admin_view: boolean }>(
`/audit-logs${params ? '?' + new URLSearchParams(params).toString() : ''}`,
{},
true,
requestConfig,
),
// List users visible to the calling admin
listUsers: (params?: Record<string, string>, requestConfig?: RequestConfig) =>
request<{ users: User[]; count: number; page: number; per_page: number; pages: number }>(
`/admin/users${params ? '?' + new URLSearchParams(params).toString() : ''}`,
{},
true,
requestConfig,
),
// Get a single user's profile + SSH keys (admin view)
getUser: (userId: string, requestConfig?: RequestConfig) =>
request<{ user: User; ssh_keys: SSHKey[] }>(`/admin/users/${userId}`, {}, true, requestConfig),
2026-03-01 16:50:19 +05:45
// Update a user's role in a shared org (admin action)
updateUserRole: (orgId: string, userId: string, role: string, requestConfig?: RequestConfig) =>
request<{ member: OrganizationMember }>(`/organizations/${orgId}/members/${userId}/role`, {
method: 'PATCH',
body: JSON.stringify({ role }),
}, true, requestConfig),
// List application-level OAuth provider configurations
listOAuthProviders: (requestConfig?: RequestConfig) =>
request<{ providers: { id: string; name: string; is_configured: boolean; is_enabled: boolean; client_id: string | null }[] }>(
'/admin/oauth/providers', {}, true, requestConfig,
),
// Create or update an application-level OAuth provider
configureOAuthProvider: (provider: string, clientId: string, clientSecret: string, isEnabled: boolean, requestConfig?: RequestConfig) =>
request<{ provider: { id: string; client_id: string; is_enabled: boolean } }>(
`/admin/oauth/providers/${provider}`,
{ method: 'PUT', body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, is_enabled: isEnabled }) },
true,
requestConfig,
),
// Delete an application-level OAuth provider
deleteOAuthProvider: (provider: string, requestConfig?: RequestConfig) =>
request<Record<string, never>>(`/admin/oauth/providers/${provider}`, { method: 'DELETE' }, true, requestConfig),
// Suspend a user account (blocks login & CA issuance)
suspendUser: (userId: string, requestConfig?: RequestConfig) =>
request<{ user: User }>(`/admin/users/${userId}/suspend`, { method: 'POST' }, true, requestConfig),
// Restore a suspended user to active status
unsuspendUser: (userId: string, requestConfig?: RequestConfig) =>
request<{ user: User }>(`/admin/users/${userId}/unsuspend`, { method: 'POST' }, true, requestConfig),
2026-03-04 18:43:12 +05:45
// Force-verify a user's email and activate their account (clears stale verification tokens)
adminVerifyUserEmail: (userId: string, requestConfig?: RequestConfig) =>
request<{ user: User }>(`/admin/users/${userId}/verify-email`, { method: 'POST' }, true, requestConfig),
// Permanently delete a user — revokes certs, cascades DB delete, unrecoverable
hardDeleteUser: (userId: string, requestConfig?: RequestConfig) =>
request<{ deleted_user_id: string; deleted_user_email: string; ssh_keys_deleted: number; certs_revoked: number }>(
`/admin/users/${userId}/delete`,
{ method: 'POST', body: JSON.stringify({ confirm: true }) },
true,
requestConfig,
),
2026-03-04 18:43:12 +05:45
// Get all MFA methods configured for a user (admin view)
getUserMfa: (userId: string, requestConfig?: RequestConfig) =>
request<{ user: { id: string; email: string; full_name: string | null }; mfa_methods: AdminMfaMethod[] }>(
`/admin/users/${userId}/mfa`,
{},
true,
requestConfig,
),
// Remove an MFA method for a user (admin action — use when user lost access)
// method_type: 'totp' | 'webauthn' | 'all'
// credentialId: optional WebAuthn credential ID to remove a single passkey
removeUserMfa: (userId: string, methodType: 'totp' | 'webauthn' | 'all', credentialId?: string, requestConfig?: RequestConfig) => {
const qs = credentialId ? `?credential_id=${encodeURIComponent(credentialId)}` : '';
return request<{ removed_methods: string[]; removed_count: number; user: { id: string; email: string } }>(
`/admin/users/${userId}/mfa/${methodType}${qs}`,
{ method: 'DELETE' },
true,
requestConfig,
);
},
// Get linked OAuth/OIDC accounts for a user (admin view)
getUserLinkedAccounts: (userId: string, requestConfig?: RequestConfig) =>
request<{
user: { id: string; email: string; full_name: string | null };
linked_accounts: AdminLinkedAccount[];
total_auth_methods: number;
}>(
`/admin/users/${userId}/linked-accounts`,
{},
true,
requestConfig,
),
// Unlink an OAuth/OIDC provider from a user's account (admin action)
// provider: provider name ('google', 'github', 'microsoft', 'oidc') or method UUID
adminUnlinkUserProvider: (userId: string, provider: string, requestConfig?: RequestConfig) =>
request<{ provider: string; user: { id: string; email: string } }>(
`/admin/users/${userId}/linked-accounts/${encodeURIComponent(provider)}`,
{ method: 'DELETE' },
true,
requestConfig,
),
// Set or reset a user's password (admin action — no current password needed)
// Creates the password auth method if the user doesn't have one (e.g. OAuth-only users)
adminSetUserPassword: (userId: string, password: string, requestConfig?: RequestConfig) =>
request<{ user: { id: string; email: string } }>(
`/admin/users/${userId}/password`,
{ method: 'POST', body: JSON.stringify({ password }) },
true,
requestConfig,
),
2026-03-01 16:50:19 +05:45
// Get the cert policy for a department
getDeptCertPolicy: (orgId: string, deptId: string, requestConfig?: RequestConfig) =>
request<{ cert_policy: DeptCertPolicy }>(`/organizations/${orgId}/departments/${deptId}/cert-policy`, {}, true, requestConfig),
// Create or update the cert policy for a department
setDeptCertPolicy: (orgId: string, deptId: string, policy: Partial<DeptCertPolicy>, requestConfig?: RequestConfig) =>
request<{ cert_policy: DeptCertPolicy }>(`/organizations/${orgId}/departments/${deptId}/cert-policy`, {
method: 'PUT',
body: JSON.stringify(policy),
}, true, requestConfig),
// Get SSH certificates issued to a user (admin view)
getUserSshCertificates: (userId: string, params?: {
status?: string;
active?: string;
cert_type?: string;
page?: number;
per_page?: number;
}, requestConfig?: RequestConfig) => {
const qs = params ? '?' + new URLSearchParams(
Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])
).toString() : '';
return request<{
user: { id: string; email: string; full_name: string };
certificates: AdminUserSshCertificate[];
count: number;
page: number;
per_page: number;
pages: number;
}>(`/admin/users/${userId}/ssh-certificates${qs}`, {}, true, requestConfig);
},
2026-01-06 15:33:03 +00:00
},
2026-01-12 06:28:36 +00:00
superadmin: {
getUserAuditLogs: (userId: string, params?: Record<string, string>, requestConfig?: RequestConfig) =>
request<{ audit_logs: AuditLogEntry[]; count: number; page: number; per_page: number; pages: number; user: User }>(
`/superadmin/users/${userId}/audit-logs${params ? '?' + new URLSearchParams(params).toString() : ''}`,
{},
true,
requestConfig,
),
exportUserAuditLogs: async (userId: string, params?: Record<string, string>): Promise<void> => {
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
const token = tokenManager.getToken();
const res = await fetch(`${config.api.baseUrl}/superadmin/users/${userId}/audit-logs/export${qs}`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!res.ok) throw new ApiError('Export failed', res.status, 'EXPORT_ERROR');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `user_${userId}_audit_logs.csv`;
a.click();
URL.revokeObjectURL(url);
},
},
2026-01-12 06:28:36 +00:00
totp: {
// Initiate TOTP enrollment - returns secret, QR code, and backup codes
enroll: () =>
request<TotpEnrollResponse>('/auth/totp/enroll', {
method: 'POST',
}),
2026-01-14 02:10:23 +00:00
// Verify TOTP enrollment - wrong code should not log user out
2026-01-12 06:28:36 +00:00
verifyEnrollment: (code: string) =>
request<{ message: string }>('/auth/totp/verify-enrollment', {
method: 'POST',
body: JSON.stringify({ code }),
2026-01-14 02:10:23 +00:00
}, true, { clearTokenOn401: false }),
2026-01-12 06:28:36 +00:00
// Verify TOTP code during login (no auth required - uses session state)
2026-01-14 07:21:55 +00:00
verify: async (code: string, isBackupCode = false): Promise<TotpVerifyResponse> => {
const response = await request<TotpVerifyResponse>('/auth/totp/verify', {
2026-01-12 06:28:36 +00:00
method: 'POST',
body: JSON.stringify({ code, is_backup_code: isBackupCode }),
2026-01-14 07:21:55 +00:00
credentials: 'include', // Required for TOTP session tracking
}, false);
2026-01-15 23:15:04 +00:00
if (response.token) {
tokenManager.setToken(response.token, response.expires_at ?? null);
2026-01-14 07:21:55 +00:00
}
return response;
},
2026-01-12 06:28:36 +00:00
2026-01-20 15:54:11 +10:30
// Verify TOTP code with an mfa_token (used after OAuth callback when MFA is required)
verifyWithMfaToken: async (code: string, mfaToken: string, isBackupCode = false): Promise<TotpVerifyResponse> => {
const response = await request<TotpVerifyResponse>('/auth/totp/verify', {
method: 'POST',
body: JSON.stringify({ code, mfa_token: mfaToken, is_backup_code: isBackupCode }),
credentials: 'include',
}, false);
if (response.token) {
tokenManager.setToken(response.token, response.expires_at ?? null);
}
return response;
},
2026-01-12 06:28:36 +00:00
// Get TOTP status
status: () =>
request<TotpStatusResponse>('/auth/totp/status'),
2026-01-14 02:10:23 +00:00
// Disable TOTP - wrong password should not log user out
2026-03-04 18:43:12 +05:45
disable: (password?: string | null) =>
2026-01-12 06:28:36 +00:00
request<{ message: string }>('/auth/totp/disable', {
method: 'DELETE',
2026-03-04 18:43:12 +05:45
body: JSON.stringify({ password: password || null }),
2026-01-14 02:10:23 +00:00
}, true, { clearTokenOn401: false }),
2026-01-12 06:28:36 +00:00
2026-01-14 02:10:23 +00:00
// Regenerate backup codes - wrong password should not log user out
2026-01-12 06:28:36 +00:00
regenerateBackupCodes: (password: string) =>
request<{ backup_codes: string[] }>('/auth/totp/regenerate-backup-codes', {
method: 'POST',
body: JSON.stringify({ password }),
2026-01-14 02:10:23 +00:00
}, true, { clearTokenOn401: false }),
2026-01-12 06:28:36 +00:00
},
2026-01-14 15:32:30 +00:00
webauthn: {
// Get WebAuthn status
status: () =>
request<WebAuthnStatusResponse>('/auth/webauthn/status'),
// List all passkeys for current user
listCredentials: () =>
request<WebAuthnCredentialsResponse>('/auth/webauthn/credentials'),
// Begin passkey registration (returns raw WebAuthn options)
beginRegistration: async (): Promise<Record<string, unknown>> => {
const response = await fetch(`${config.api.baseUrl}/auth/webauthn/register/begin`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${tokenManager.getToken()}`,
},
});
if (!response.ok) {
const error = await response.json();
throw new ApiError(
error.message || 'Failed to begin registration',
error.code || response.status,
error.error?.type || 'WEBAUTHN_ERROR',
error.error?.details || {}
);
}
// Returns raw WebAuthn options (not wrapped in standard response)
return response.json();
},
// Complete passkey registration
completeRegistration: (credential: Record<string, unknown>, name?: string) =>
request<{ message: string; credential_id: string }>('/auth/webauthn/register/complete', {
method: 'POST',
body: JSON.stringify({ ...credential, name }),
}),
// Begin passkey login (returns raw WebAuthn options)
beginLogin: async (email: string): Promise<Record<string, unknown>> => {
const response = await fetch(`${config.api.baseUrl}/auth/webauthn/login/begin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Required for session cookie
2026-01-14 15:32:30 +00:00
body: JSON.stringify({ email }),
});
if (!response.ok) {
const error = await response.json();
throw new ApiError(
error.message || 'No passkeys found for this account',
error.code || response.status,
error.error?.type || 'WEBAUTHN_ERROR',
error.error?.details || {}
);
}
// Returns raw WebAuthn options (not wrapped in standard response)
return response.json();
},
// Complete passkey login
completeLogin: async (assertion: Record<string, unknown>): Promise<WebAuthnLoginCompleteResponse> => {
const response = await fetch(`${config.api.baseUrl}/auth/webauthn/login/complete`, {
2026-01-14 15:32:30 +00:00
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Required for session cookie
2026-01-14 15:32:30 +00:00
body: JSON.stringify(assertion),
});
const json: ApiResponse<WebAuthnLoginCompleteResponse> = await response.json();
if (!json.success) {
throw new ApiError(
json.message || 'Authentication failed',
json.code,
json.error?.type || 'AUTHENTICATION_ERROR',
json.error?.details || {}
);
}
2026-01-14 15:32:30 +00:00
// Store token after successful passkey login
if (json.data?.token) {
tokenManager.setToken(json.data.token, json.data.expires_at ?? null);
2026-01-14 15:32:30 +00:00
}
return json.data as WebAuthnLoginCompleteResponse;
2026-01-14 15:32:30 +00:00
},
// Rename a passkey
renameCredential: (credentialId: string, name: string) =>
request<{ message: string }>(`/auth/webauthn/credentials/${credentialId}`, {
method: 'PATCH',
body: JSON.stringify({ name }),
}),
// Delete a passkey
deleteCredential: (credentialId: string) =>
request<{ message: string }>(`/auth/webauthn/credentials/${credentialId}`, {
method: 'DELETE',
}),
},
2026-01-16 17:31:25 +10:30
policies: {
// Get organization security policy
getOrgPolicy: (orgId: string, requestConfig?: RequestConfig) =>
request<OrgPolicyResponse>(`/organizations/${orgId}/security-policy`, {}, true, requestConfig),
// Update organization security policy
updateOrgPolicy: (orgId: string, body: UpdateOrgPolicyDto, requestConfig?: RequestConfig) =>
request<OrgPolicyResponse>(`/organizations/${orgId}/security-policy`, {
method: 'PUT',
body: JSON.stringify(body),
}, true, requestConfig),
// List organization compliance (paginated)
listOrgCompliance: (orgId: string, params: Record<string, string>, requestConfig?: RequestConfig) =>
request<OrgCompliancePage>(
`/organizations/${orgId}/mfa-compliance?${new URLSearchParams(params)}`,
{},
true,
requestConfig
),
// Get current user's MFA compliance summary
getMyCompliance: () =>
request<MfaComplianceSummary>('/users/me/mfa-compliance'),
},
2026-01-20 15:54:11 +10:30
externalAuth: {
2026-01-21 03:09:38 +10:30
// List available providers
2026-01-20 15:54:11 +10:30
listProviders: () =>
2026-01-21 03:09:38 +10:30
request<ExternalProvidersResponse>('/auth/external/providers'),
2026-01-20 15:54:11 +10:30
2026-01-21 03:09:38 +10:30
// Get linked accounts for current user
2026-01-20 15:54:11 +10:30
listLinkedAccounts: () =>
request<LinkedAccountsResponse>('/auth/external/linked-accounts'),
// Initiate OAuth login flow — returns authorization_url to redirect the browser to
initiateLogin: (provider: string, options?: { organization_id?: string; flow?: string; oidc_session_id?: string }) => {
const params = new URLSearchParams({ flow: options?.flow ?? 'login' });
if (options?.organization_id) params.set('organization_id', options.organization_id);
if (options?.oidc_session_id) params.set('oidc_session_id', options.oidc_session_id);
return request<OAuthAuthorizeResponse>(`/auth/external/${provider}/authorize?${params.toString()}`, {
2026-01-21 03:09:38 +10:30
method: 'GET',
2026-01-20 15:54:11 +10:30
credentials: 'include',
}, false);
},
2026-01-20 15:54:11 +10:30
2026-01-21 03:09:38 +10:30
// Initiate account linking flow (requires auth)
initiateLink: (provider: string) =>
2026-01-20 15:54:11 +10:30
request<OAuthAuthorizeResponse>(`/auth/external/${provider}/link`, {
method: 'POST',
credentials: 'include',
}),
2026-01-21 03:09:38 +10:30
// Unlink an external account
unlinkAccount: (provider: string) =>
request<{ message: string }>(`/auth/external/${provider}/unlink`, {
method: 'DELETE',
credentials: 'include',
}),
2026-01-20 15:54:11 +10:30
},
organizations: {
// Create a new organization (caller becomes owner)
create: (name: string, slug: string, description?: string) =>
request<{ organization: Organization }>('/organizations', {
method: 'POST',
body: JSON.stringify({ name, slug, description }),
}, true),
// Get organization by ID
getById: (orgId: string, requestConfig?: RequestConfig) =>
request<{ organization: Organization; member_count: number }>(`/organizations/${orgId}`, {}, true, requestConfig),
2026-03-03 23:23:18 +05:45
// Delete an organization (owner only; pass confirm=true when other members exist)
deleteOrganization: (orgId: string, confirm?: boolean, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}`, {
method: 'DELETE',
...(confirm ? { body: JSON.stringify({ confirm: true }) } : {}),
}, true, requestConfig),
// Get organization members
getMembers: (orgId: string, requestConfig?: RequestConfig) =>
request<{ members: OrganizationMember[]; count: number }>(`/organizations/${orgId}/members`, {}, true, requestConfig),
// Add member to organization
addMember: (orgId: string, email: string, role: string, requestConfig?: RequestConfig) =>
request<{ member: OrganizationMember }>(`/organizations/${orgId}/members`, {
method: 'POST',
body: JSON.stringify({ email, role }),
}, true, requestConfig),
// Remove member from organization
removeMember: (orgId: string, userId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/members/${userId}`, {
method: 'DELETE',
}, true, requestConfig),
// Update member role
updateMemberRole: (orgId: string, userId: string, role: string, requestConfig?: RequestConfig) =>
request<{ member: OrganizationMember }>(`/organizations/${orgId}/members/${userId}/role`, {
method: 'PATCH',
body: JSON.stringify({ role }),
}, true, requestConfig),
// Get organization audit logs
getAuditLogs: (orgId: string, params?: Record<string, string>, requestConfig?: RequestConfig) =>
2026-03-06 14:19:54 +05:45
request<{ audit_logs: AuditLogEntry[]; count: number; page: number; per_page: number; pages: number }>(
`/organizations/${orgId}/audit-logs${params ? '?' + new URLSearchParams(params).toString() : ''}`,
{},
true,
requestConfig
),
// Get departments
getDepartments: (orgId: string, requestConfig?: RequestConfig) =>
request<{ departments: Department[]; count: number }>(`/organizations/${orgId}/departments`, {}, true, requestConfig),
// Create department
2026-03-08 18:08:42 +05:45
createDepartment: (orgId: string, name: string, description?: string, canSudo?: boolean, requestConfig?: RequestConfig) =>
request<{ department: Department }>(`/organizations/${orgId}/departments`, {
method: 'POST',
2026-03-08 18:08:42 +05:45
body: JSON.stringify({ name, description, can_sudo: canSudo }),
}, true, requestConfig),
// Update department
2026-03-08 18:08:42 +05:45
updateDepartment: (orgId: string, deptId: string, data: { name?: string; description?: string; can_sudo?: boolean }, requestConfig?: RequestConfig) =>
request<{ department: Department }>(`/organizations/${orgId}/departments/${deptId}`, {
method: 'PATCH',
body: JSON.stringify(data),
}, true, requestConfig),
// Delete department
deleteDepartment: (orgId: string, deptId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/departments/${deptId}`, {
method: 'DELETE',
}, true, requestConfig),
// Get department members
getDepartmentMembers: (orgId: string, deptId: string, requestConfig?: RequestConfig) =>
request<{ members: DepartmentMember[]; count: number }>(`/organizations/${orgId}/departments/${deptId}/members`, {}, true, requestConfig),
// Add member to department
addDepartmentMember: (orgId: string, deptId: string, email: string, requestConfig?: RequestConfig) =>
request<{ member: DepartmentMember }>(`/organizations/${orgId}/departments/${deptId}/members`, {
method: 'POST',
body: JSON.stringify({ email }),
}, true, requestConfig),
// Remove member from department
removeDepartmentMember: (orgId: string, deptId: string, userId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/departments/${deptId}/members/${userId}`, {
method: 'DELETE',
}, true, requestConfig),
// Get principals
getPrincipals: (orgId: string, requestConfig?: RequestConfig) =>
request<{ principals: Principal[]; count: number }>(`/organizations/${orgId}/principals`, {}, true, requestConfig),
// Create principal
createPrincipal: (orgId: string, name: string, description?: string, requestConfig?: RequestConfig) =>
request<{ principal: Principal }>(`/organizations/${orgId}/principals`, {
method: 'POST',
body: JSON.stringify({ name, description }),
}, true, requestConfig),
// Update principal
updatePrincipal: (orgId: string, principalId: string, data: { name?: string; description?: string }, requestConfig?: RequestConfig) =>
request<{ principal: Principal }>(`/organizations/${orgId}/principals/${principalId}`, {
method: 'PATCH',
body: JSON.stringify(data),
}, true, requestConfig),
// Delete principal
deletePrincipal: (orgId: string, principalId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/principals/${principalId}`, {
method: 'DELETE',
}, true, requestConfig),
// Get principal members
getPrincipalMembers: (orgId: string, principalId: string, requestConfig?: RequestConfig) =>
request<{ members: PrincipalMember[]; count: number }>(`/organizations/${orgId}/principals/${principalId}/members`, {}, true, requestConfig),
// Add member to principal
addPrincipalMember: (orgId: string, principalId: string, email: string, requestConfig?: RequestConfig) =>
request<{ member: PrincipalMember }>(`/organizations/${orgId}/principals/${principalId}/members`, {
method: 'POST',
body: JSON.stringify({ email }),
}, true, requestConfig),
// Remove member from principal
removePrincipalMember: (orgId: string, principalId: string, userId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/principals/${principalId}/members/${userId}`, {
method: 'DELETE',
}, true, requestConfig),
// Link principal to department
linkPrincipalToDepartment: (orgId: string, principalId: string, departmentId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/principals/${principalId}/departments/${departmentId}`, {
method: 'POST',
}, true, requestConfig),
// Unlink principal from department
unlinkPrincipalFromDepartment: (orgId: string, principalId: string, departmentId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/principals/${principalId}/departments/${departmentId}`, {
method: 'DELETE',
}, true, requestConfig),
// Get departments linked to a principal
getPrincipalDepartments: (orgId: string, principalId: string, requestConfig?: RequestConfig) =>
request<{ departments: Department[]; count: number }>(`/organizations/${orgId}/principals/${principalId}/departments`, {}, true, requestConfig),
// Get principals linked to a department
getDepartmentPrincipals: (orgId: string, deptId: string, requestConfig?: RequestConfig) =>
request<{ principals: Principal[]; count: number }>(`/organizations/${orgId}/departments/${deptId}/principals`, {}, true, requestConfig),
// Create invite token
createInvite: (orgId: string, email: string, role: string, requestConfig?: RequestConfig) =>
request<{ invite: OrgInvite }>(`/organizations/${orgId}/invites`, {
method: 'POST',
body: JSON.stringify({ email, role }),
}, true, requestConfig),
2026-03-01 16:50:19 +05:45
// List pending invites for an organization
getInvites: (orgId: string, requestConfig?: RequestConfig) =>
request<{ invites: OrgInvite[] }>(`/organizations/${orgId}/invites`, {}, true, requestConfig),
// Cancel (delete) an invite
cancelInvite: (orgId: string, inviteId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/invites/${inviteId}`, {
method: 'DELETE',
}, true, requestConfig),
// List OIDC clients
getClients: (orgId: string, requestConfig?: RequestConfig) =>
request<{ clients: OIDCClient[]; count: number }>(`/organizations/${orgId}/clients`, {}, true, requestConfig),
// Create OIDC client
2026-05-19 15:13:51 +00:00
createClient: (orgId: string, name: string, redirect_uris: string[], allowed_cors_origins?: string[] | null, requestConfig?: RequestConfig) =>
request<{ client: OIDCClientWithSecret }>(`/organizations/${orgId}/clients`, {
method: 'POST',
2026-05-19 15:13:51 +00:00
body: JSON.stringify({ name, redirect_uris, allowed_cors_origins }),
}, true, requestConfig),
// Delete OIDC client
deleteClient: (orgId: string, clientId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/clients/${clientId}`, {
method: 'DELETE',
}, true, requestConfig),
2026-05-19 15:13:51 +00:00
// Update OIDC client (name, redirect_uris, and/or allowed_cors_origins)
updateClient: (orgId: string, clientId: string, data: { name?: string; redirect_uris?: string[]; allowed_cors_origins?: string[] | null }, requestConfig?: RequestConfig) =>
2026-03-31 12:56:52 +05:45
request<{ client: OIDCClient }>(`/organizations/${orgId}/clients/${clientId}`, {
method: 'PATCH',
body: JSON.stringify(data),
}, true, requestConfig),
// Send MFA reminder to a member
sendMfaReminder: (orgId: string, userId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/members/${userId}/send-mfa-reminder`, {
method: 'POST',
}, true, requestConfig),
// Transfer organization ownership to another member
transferOwnership: (orgId: string, newOwnerUserId: string, requestConfig?: RequestConfig) =>
request<{ previous_owner: OrganizationMember; new_owner: OrganizationMember }>(
`/organizations/${orgId}/transfer-ownership`,
{ method: 'POST', body: JSON.stringify({ new_owner_user_id: newOwnerUserId }) },
true,
requestConfig,
),
// List Certificate Authorities for an org
getCAs: (orgId: string, requestConfig?: RequestConfig) =>
request<{ cas: OrgCA[]; count: number }>(`/organizations/${orgId}/cas`, {}, true, requestConfig),
// Create a new Certificate Authority for an org
createCA: (orgId: string, data: { name: string; description?: string; ca_type?: 'user' | 'host'; key_type?: 'ed25519' | 'rsa' | 'ecdsa'; default_cert_validity_hours?: number; max_cert_validity_hours?: number }, requestConfig?: RequestConfig) =>
request<{ ca: OrgCA }>(`/organizations/${orgId}/cas`, {
method: 'POST',
body: JSON.stringify(data),
}, true, requestConfig),
// Update CA configuration
updateCA: (orgId: string, caId: string, data: { default_cert_validity_hours?: number; max_cert_validity_hours?: number }, requestConfig?: RequestConfig) =>
request<{ ca: OrgCA }>(`/organizations/${orgId}/cas/${caId}`, {
method: 'PATCH',
body: JSON.stringify(data),
}, true, requestConfig),
// Rotate (replace) a CA's key pair — returns updated CA + old_fingerprint
rotateCA: (orgId: string, caId: string, data?: { key_type?: 'ed25519' | 'rsa' | 'ecdsa'; reason?: string }, requestConfig?: RequestConfig) =>
request<{ ca: OrgCA; old_fingerprint: string }>(`/organizations/${orgId}/cas/${caId}/rotate`, {
method: 'POST',
body: JSON.stringify(data ?? {}),
}, true, requestConfig),
// Soft-delete a CA
deleteCA: (orgId: string, caId: string, requestConfig?: RequestConfig) =>
request<{ ca_id: string }>(`/organizations/${orgId}/cas/${caId}`, {
method: 'DELETE',
}, true, requestConfig),
2026-03-08 18:08:42 +05:45
// Get API keys for organization
getApiKeys: (orgId: string, requestConfig?: RequestConfig) =>
request<{ api_keys: OrganizationApiKey[]; count: number }>(`/organizations/${orgId}/api-keys`, {}, true, requestConfig),
// Create new API key
createApiKey: (orgId: string, name: string, description?: string, requestConfig?: RequestConfig) =>
request<{ api_key: OrganizationApiKey & { key?: string } }>(`/organizations/${orgId}/api-keys`, {
method: 'POST',
body: JSON.stringify({ name, description }),
}, true, requestConfig),
// Update API key
updateApiKey: (orgId: string, keyId: string, data: { name?: string; description?: string }, requestConfig?: RequestConfig) =>
request<{ api_key: OrganizationApiKey }>(`/organizations/${orgId}/api-keys/${keyId}`, {
method: 'PATCH',
body: JSON.stringify(data),
}, true, requestConfig),
// Delete API key
deleteApiKey: (orgId: string, keyId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/organizations/${orgId}/api-keys/${keyId}`, {
method: 'DELETE',
}, true, requestConfig),
// Get certificate audit logs for organization
getCertificateAuditLogs: (orgId: string, params?: Record<string, string>, requestConfig?: RequestConfig) =>
request<{ audit_logs: CertificateAuditLog[]; count: number; page: number; per_page: number; pages: number }>(
`/organizations/${orgId}/certificates/audit${params ? '?' + new URLSearchParams(params).toString() : ''}`,
{},
true,
requestConfig
),
},
invites: {
// Get invite details by token (unauthenticated)
getInfo: (token: string) =>
2026-03-01 16:50:19 +05:45
request<{ email: string; organization: { id: string; name: string }; role: string; user_exists?: boolean }>(
`/invites/${token}`,
{},
false,
),
2026-03-04 18:43:12 +05:45
// Accept invite — sends Bearer token if present (OAuth users skip password)
2026-03-01 16:50:19 +05:45
accept: (token: string, full_name?: string, password?: string) =>
request<LoginResponse>(
`/invites/${token}/accept`,
{
method: 'POST',
body: JSON.stringify({ full_name, password, password_confirm: password }),
},
2026-03-04 18:43:12 +05:45
true,
),
},
contact: {
submit: (data: {
email: string;
name?: string;
company?: string;
enquiry_type: string;
message?: string;
interest_area?: string;
_hp?: string;
}) =>
request<void>('/contact', {
method: 'POST',
body: JSON.stringify(data),
}, false),
},
ssh: {
// List all SSH keys for the current user
listKeys: (requestConfig?: RequestConfig) =>
request<SSHKeysResponse>('/ssh/keys', {}, true, requestConfig),
// Add a new SSH public key
addKey: (public_key: string, description?: string, requestConfig?: RequestConfig) =>
request<SSHKey>('/ssh/keys', {
method: 'POST',
body: JSON.stringify({ public_key, description }),
}, true, requestConfig),
// Delete an SSH key
deleteKey: (keyId: string, requestConfig?: RequestConfig) =>
request<{ status: string }>(`/ssh/keys/${keyId}`, {
method: 'DELETE',
}, true, requestConfig),
// Update SSH key description
updateKeyDescription: (keyId: string, description: string, requestConfig?: RequestConfig) =>
request<SSHKey>(`/ssh/keys/${keyId}/update-description`, {
method: 'PATCH',
body: JSON.stringify({ description }),
}, true, requestConfig),
// Get a verification challenge for a key
getChallenge: (keyId: string, requestConfig?: RequestConfig) =>
request<SSHChallengeResponse>(`/ssh/keys/${keyId}/verify`, {}, true, requestConfig),
// Submit signature to verify key ownership
verifyKey: (keyId: string, signature: string, requestConfig?: RequestConfig) =>
request<SSHVerifyResponse>(`/ssh/keys/${keyId}/verify`, {
method: 'POST',
body: JSON.stringify({ signature, action: 'verify_signature' }),
}, true, requestConfig),
// Sign a certificate for the given key
signCertificate: (key_id: string, principals?: string[], cert_type?: 'user' | 'host', expiry_hours?: number, organization_id?: string, requestConfig?: RequestConfig) =>
request<SSHSignResponse>('/ssh/sign', {
method: 'POST',
body: JSON.stringify({ key_id, principals, cert_type, expiry_hours, organization_id }),
}, true, requestConfig),
// Issue a host certificate by submitting a raw server host public key
// (admin-only; does not require a pre-registered SSHKey record)
signHostCert: (
hostPublicKey: string,
principals: string[],
validityHours: number,
caId: string,
requestConfig?: RequestConfig,
) =>
request<SSHSignResponse>('/ssh/sign/host', {
method: 'POST',
body: JSON.stringify({
host_public_key: hostPublicKey,
principals,
validity_hours: validityHours,
ca_id: caId,
}),
}, true, requestConfig),
2026-03-01 16:50:19 +05:45
// Get the merged department certificate policy for the current user (used in sign dialog)
getMyDeptCertPolicy: (requestConfig?: RequestConfig) =>
request<{ policy: DeptCertPolicy }>('/ssh/dept-cert-policy', {}, true, requestConfig),
// List issued certificates for the current user
listCertificates: (requestConfig?: RequestConfig) =>
request<{ certificates: SSHCertificate[]; count: number }>('/ssh/certificates', {}, true, requestConfig),
// Get a single certificate (includes full cert text)
getCertificate: (certId: string, requestConfig?: RequestConfig) =>
request<SSHCertificate>(`/ssh/certificates/${certId}`, {}, true, requestConfig),
// Revoke a certificate
revokeCertificate: (certId: string, reason?: string, requestConfig?: RequestConfig) =>
request<{ status: string; cert_id: string; reason: string }>(`/ssh/certificates/${certId}/revoke`, {
method: 'POST',
body: JSON.stringify({ reason }),
}, true, requestConfig),
// Get the CA public key for the current user's org
getCaPublicKey: (requestConfig?: RequestConfig) =>
request<{ public_key: string; fingerprint: string; ca_name: string; source: string }>('/ssh/ca/public-key', {}, true, requestConfig),
// Add SSH key on behalf of another user (admin)
adminAddKey: (userId: string, public_key: string, description?: string, requestConfig?: RequestConfig) =>
request<SSHKey>(`/ssh/keys/admin/${userId}`, {
method: 'POST',
body: JSON.stringify({ public_key, description }),
}, true, requestConfig),
// List CA permissions for a CA
listCaPermissions: (caId: string, requestConfig?: RequestConfig) =>
request<{ ca_id: string; permissions: CAPermission[]; open_to_all: boolean }>(`/ssh/ca/${caId}/permissions`, {}, true, requestConfig),
// Grant a user permission on a CA
addCaPermission: (caId: string, user_id: string, permission: 'sign' | 'admin', requestConfig?: RequestConfig) =>
request<{ message: string; permission: CAPermission }>(`/ssh/ca/${caId}/permissions`, {
method: 'POST',
body: JSON.stringify({ user_id, permission }),
}, true, requestConfig),
// Revoke a user's CA permission
removeCaPermission: (caId: string, userId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(`/ssh/ca/${caId}/permissions/${userId}`, {
method: 'DELETE',
}, true, requestConfig),
},
zerotier: {
// ── Portal Networks ─────────────────────────────────────────────────────────
listNetworks: (orgId: string, includeInactive = false, requestConfig?: RequestConfig) =>
request<{ networks: PortalNetwork[]; count: number }>(
`/organizations/${orgId}/networks${includeInactive ? "?include_inactive=true" : ""}`,
{}, true, requestConfig,
),
createNetwork: (orgId: string, data: {
name: string;
zerotier_network_id: string;
description?: string;
environment?: string;
request_mode?: string;
default_activation_lifetime_minutes?: number;
max_activation_lifetime_minutes?: number;
}, requestConfig?: RequestConfig) =>
request<{ network: PortalNetwork }>(
`/organizations/${orgId}/networks`,
{ method: "POST", body: JSON.stringify(data) },
true, requestConfig,
),
getNetwork: (orgId: string, networkId: string, requestConfig?: RequestConfig) =>
request<{ network: PortalNetwork }>(
`/organizations/${orgId}/networks/${networkId}`,
{}, true, requestConfig,
),
updateNetwork: (orgId: string, networkId: string, data: Record<string, unknown>, requestConfig?: RequestConfig) =>
request<{ network: PortalNetwork }>(
`/organizations/${orgId}/networks/${networkId}`,
{ method: "PATCH", body: JSON.stringify(data) },
true, requestConfig,
),
deleteNetwork: (orgId: string, networkId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(
`/organizations/${orgId}/networks/${networkId}`,
{ method: "DELETE" }, true, requestConfig,
),
2026-03-29 21:33:37 +05:45
/** List all ZeroTier networks from the org's controller/account, annotated
* with whether each is already managed as a portal network. */
listAvailableZtNetworks: (orgId: string, requestConfig?: RequestConfig) =>
request<{ networks: AvailableZtNetwork[]; count: number; zt_error?: string }>(
`/organizations/${orgId}/zerotier/available-networks`,
{}, true, requestConfig,
),
getNetworkMembers: (orgId: string, networkId: string, requestConfig?: RequestConfig) =>
request<{ memberships: DeviceNetworkMembership[]; count: number }>(
`/organizations/${orgId}/networks/${networkId}/members`,
{}, true, requestConfig,
),
getNetworkPendingRequests: (orgId: string, networkId: string, requestConfig?: RequestConfig) =>
request<{ requests: UserNetworkApproval[]; count: number }>(
`/organizations/${orgId}/networks/${networkId}/requests`,
{}, true, requestConfig,
),
// ── Devices ───────────────────────────────────────────────────────────────
listDevices: (orgId: string, requestConfig?: RequestConfig) =>
request<{ devices: Device[]; count: number }>(
`/organizations/${orgId}/devices`, {}, true, requestConfig,
),
registerDevice: (orgId: string, data: {
node_id: string;
nickname?: string;
hostname?: string;
asset_tag?: string;
serial_number?: string;
}, requestConfig?: RequestConfig) =>
request<{ device: Device; memberships_created: number }>(
`/organizations/${orgId}/devices`,
{ method: "POST", body: JSON.stringify(data) },
true, requestConfig,
),
getDevice: (orgId: string, deviceId: string, requestConfig?: RequestConfig) =>
request<{ device: Device }>(
`/organizations/${orgId}/devices/${deviceId}`,
{}, true, requestConfig,
),
updateDevice: (orgId: string, deviceId: string, data: {
nickname?: string;
hostname?: string;
asset_tag?: string;
serial_number?: string;
}, requestConfig?: RequestConfig) =>
request<{ device: Device }>(
`/organizations/${orgId}/devices/${deviceId}`,
{ method: "PATCH", body: JSON.stringify(data) },
true, requestConfig,
),
removeDevice: (orgId: string, deviceId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(
`/organizations/${orgId}/devices/${deviceId}`,
{ method: "DELETE" }, true, requestConfig,
),
// ── Approvals ─────────────────────────────────────────────────────────────
requestAccess: (orgId: string, data: {
portal_network_id: string;
device_id: string;
justification?: string;
}, requestConfig?: RequestConfig) =>
request<{ approval: UserNetworkApproval }>(
`/organizations/${orgId}/approvals`,
{ method: "POST", body: JSON.stringify(data) },
true, requestConfig,
),
listMyApprovals: (orgId: string, requestConfig?: RequestConfig) =>
request<{ approvals: UserNetworkApproval[]; count: number }>(
`/organizations/${orgId}/approvals`, {}, true, requestConfig,
),
2026-03-29 21:33:37 +05:45
adminListAllApprovals: (orgId: string, networkId?: string, state?: string, requestConfig?: RequestConfig) =>
request<{ approvals: UserNetworkApproval[]; count: number }>(
`/organizations/${orgId}/admin/approvals${networkId || state ? `?${new URLSearchParams(Object.fromEntries(Object.entries({ network_id: networkId, state }).filter(([, v]) => v != null) as [string, string][]))}` : ""}`,
{}, true, requestConfig,
),
listPendingApprovals: (orgId: string, networkId?: string, requestConfig?: RequestConfig) =>
request<{ approvals: UserNetworkApproval[]; count: number }>(
`/organizations/${orgId}/approvals/pending${networkId ? `?network_id=${networkId}` : ""}`,
{}, true, requestConfig,
),
approveRequest: (orgId: string, approvalId: string, requestConfig?: RequestConfig) =>
request<{ approval: UserNetworkApproval }>(
`/organizations/${orgId}/approvals/${approvalId}/approve`,
{ method: "POST" }, true, requestConfig,
),
rejectRequest: (orgId: string, approvalId: string, requestConfig?: RequestConfig) =>
request<{ approval: UserNetworkApproval }>(
`/organizations/${orgId}/approvals/${approvalId}/reject`,
{ method: "POST" }, true, requestConfig,
),
revokeApproval: (orgId: string, approvalId: string, requestConfig?: RequestConfig) =>
request<{ approval: UserNetworkApproval }>(
`/organizations/${orgId}/approvals/${approvalId}/revoke`,
{ method: "POST" }, true, requestConfig,
),
assignAccess: (orgId: string, data: {
target_user_id: string;
portal_network_id: string;
justification?: string;
}, requestConfig?: RequestConfig) =>
request<{ approval: UserNetworkApproval }>(
`/organizations/${orgId}/approvals/assign`,
{ method: "POST", body: JSON.stringify(data) },
true, requestConfig,
),
// ── Memberships ────────────────────────────────────────────────────────────
listMemberships: (orgId: string, requestConfig?: RequestConfig) =>
request<{ memberships: DeviceNetworkMembership[]; count: number }>(
`/organizations/${orgId}/memberships`, {}, true, requestConfig,
),
activateMembership: (orgId: string, membershipId: string, lifetimeMinutes?: number, requestConfig?: RequestConfig) =>
request<{ session: ActivationSession; membership: DeviceNetworkMembership }>(
`/organizations/${orgId}/memberships/${membershipId}/activate`,
{ method: "POST", body: JSON.stringify({ lifetime_minutes: lifetimeMinutes }) },
true, requestConfig,
),
deactivateMembership: (orgId: string, membershipId: string, requestConfig?: RequestConfig) =>
request<{ membership: DeviceNetworkMembership }>(
`/organizations/${orgId}/memberships/${membershipId}/deactivate`,
{ method: "POST" }, true, requestConfig,
),
activateAllMemberships: (orgId: string, lifetimeMinutes?: number, requestConfig?: RequestConfig) =>
request<{ sessions: ActivationSession[]; count: number }>(
`/organizations/${orgId}/memberships/activate-all`,
{ method: "POST", body: JSON.stringify({ lifetime_minutes: lifetimeMinutes }) },
true, requestConfig,
),
joinNetworkForDevice: (orgId: string, deviceId: string, networkId: string, requestConfig?: RequestConfig) =>
request<{ membership: DeviceNetworkMembership }>(
`/organizations/${orgId}/devices/${deviceId}/join-network/${networkId}`,
{ method: "POST" },
true, requestConfig,
),
deleteMembership: (orgId: string, membershipId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(
`/organizations/${orgId}/memberships/${membershipId}`,
{ method: "DELETE" },
true, requestConfig,
),
// ── Admin ─────────────────────────────────────────────────────────────────
adminListAllMemberships: (orgId: string, requestConfig?: RequestConfig) =>
request<{ memberships: EnrichedMembership[]; count: number }>(
`/organizations/${orgId}/admin/memberships`,
{},
true,
requestConfig,
),
adminDeleteMembership: (orgId: string, membershipId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(
`/organizations/${orgId}/admin/memberships/${membershipId}`,
{ method: "DELETE" },
true, requestConfig,
),
// ── Org Members (for Add New Membership dialog) ────────────────────
getOrgMembers: (orgId: string, requestConfig?: RequestConfig) =>
request<{ members: OrgMember[]; count: number }>(
`/organizations/${orgId}/members`,
{}, true, requestConfig,
),
getUserDevices: (orgId: string, userId: string, requestConfig?: RequestConfig) =>
request<{ devices: Device[]; count: number }>(
`/organizations/${orgId}/users/${userId}/devices`,
{}, true, requestConfig,
),
// ── Sessions ──────────────────────────────────────────────────────────────
2026-05-29 06:28:26 +00:00
listUserSessions: (orgId: string, requestConfig?: RequestConfig) =>
request<{ sessions: UserSession[]; count: number }>(
`/organizations/${orgId}/sessions`, {}, true, requestConfig,
),
2026-05-29 06:28:26 +00:00
adminListSessions: (orgId: string, requestConfig?: RequestConfig) =>
request<{ sessions: AdminSession[]; count: number }>(
`/organizations/${orgId}/admin/sessions`, {}, true, requestConfig,
),
adminEndSession: (orgId: string, sessionId: string, requestConfig?: RequestConfig) =>
request<{ session: ActivationSession; message: string }>(
`/organizations/${orgId}/admin/sessions/${sessionId}/end`,
{ method: "POST", body: "{}" }, true, requestConfig,
),
// ── Kill Switch ───────────────────────────────────────────────────────────
triggerKillSwitch: (orgId: string, data: {
target_user_id: string;
scope?: string;
reason?: string;
network_ids?: string[];
}, requestConfig?: RequestConfig) =>
request<{ event: KillSwitchEvent }>(
`/organizations/${orgId}/kill-switch`,
{ method: "POST", body: JSON.stringify(data) },
true, requestConfig,
),
2026-03-29 21:33:37 +05:45
// ── ZeroTier Controller (org-scoped admin) ─────────────────────────────────
getZtStatus: (orgId: string, requestConfig?: RequestConfig) =>
request<{ status: Record<string, unknown> }>(
2026-03-29 21:33:37 +05:45
`/admin/zerotier/status?org_id=${orgId}`, {}, true, requestConfig,
),
2026-03-29 21:33:37 +05:45
listZtNetworks: (orgId: string, requestConfig?: RequestConfig) =>
request<{ networks: ZeroTierNetwork[]; count: number }>(
2026-03-29 21:33:37 +05:45
`/admin/zerotier/networks?org_id=${orgId}`, {}, true, requestConfig,
),
2026-03-29 21:33:37 +05:45
getZtNetwork: (orgId: string, networkId: string, requestConfig?: RequestConfig) =>
request<{ network: ZeroTierNetwork }>(
2026-03-29 21:33:37 +05:45
`/admin/zerotier/networks/${networkId}?org_id=${orgId}`, {}, true, requestConfig,
),
2026-03-29 21:33:37 +05:45
listZtMembers: (orgId: string, networkId: string, requestConfig?: RequestConfig) =>
request<{ members: ZeroTierMember[]; count: number }>(
2026-03-29 21:33:37 +05:45
`/admin/zerotier/networks/${networkId}/members?org_id=${orgId}`, {}, true, requestConfig,
),
triggerReconciliation: (requestConfig?: RequestConfig) =>
request<{ networks_processed: number; errors: number }>(
"/admin/zerotier/reconcile",
{ method: "POST" }, true, requestConfig,
),
2026-03-29 21:33:37 +05:45
// ── Per-org ZeroTier config ──────────────────────────────────────────────
getOrgZtConfig: (orgId: string, requestConfig?: RequestConfig) =>
request<{ zerotier_config: ZeroTierOrgConfig }>(
`/organizations/${orgId}/zerotier-config`,
{}, true, requestConfig,
),
setOrgZtConfig: (orgId: string, data: ZeroTierOrgConfigInput, requestConfig?: RequestConfig) =>
request<{ zerotier_config: ZeroTierOrgConfig; connectivity_test: { ok: boolean; error: string | null } }>(
`/organizations/${orgId}/zerotier-config`,
{ method: "PUT", body: JSON.stringify(data) },
true, requestConfig,
),
deleteOrgZtConfig: (orgId: string, requestConfig?: RequestConfig) =>
request<{ message: string }>(
`/organizations/${orgId}/zerotier-config`,
{ method: "DELETE" }, true, requestConfig,
),
},
2026-01-06 15:33:03 +00:00
};
// Organization types
export interface OrganizationMember {
id: string;
user_id: string;
organization_id: string;
role: string;
created_at: string;
updated_at: string;
user?: User;
}
export interface AuditLogEntry {
id: string;
action: string;
user_id: string | null;
organization_id: string | null;
resource_type: string | null;
resource_id: string | null;
ip_address: string | null;
user_agent: string | null;
request_id: string | null;
description: string | null;
success: boolean;
error_message: string | null;
metadata?: Record<string, unknown>;
created_at: string;
updated_at: string;
user?: User;
}
export interface Department {
id: string;
organization_id: string;
name: string;
description: string | null;
2026-03-08 18:08:42 +05:45
can_sudo: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
2026-03-01 16:50:19 +05:45
export const STANDARD_SSH_EXTENSIONS = [
'permit-X11-forwarding',
'permit-agent-forwarding',
'permit-pty',
'permit-port-forwarding',
'permit-user-rc',
] as const;
export interface DeptCertPolicy {
department_id: string;
allow_user_expiry: boolean;
default_expiry_hours: number;
max_expiry_hours: number;
allowed_extensions: string[];
custom_extensions: string[];
all_extensions?: string[];
standard_extensions?: string[];
}
export interface PendingInvite {
token: string;
organization: { id: string; name: string };
role: string;
expires_at: string;
}
export interface MyOrgMembership {
org_id: string;
org_name: string;
role: string;
departments: { id: string; name: string; description: string | null }[];
principals: { id: string; name: string; description: string | null; via_department: boolean }[];
}
export interface DepartmentMember {
id: string;
user_id: string;
department_id: string;
created_at: string;
updated_at: string;
user?: User;
}
export interface Principal {
id: string;
organization_id: string;
name: string;
description: string | null;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
export interface PrincipalMember {
id: string;
user_id: string;
principal_id: string;
created_at: string;
updated_at: string;
user?: User;
}
export interface OrgInvite {
id: string;
email: string;
role: string;
expires_at: string;
2026-03-01 16:50:19 +05:45
invite_link?: string; // only present on create response (dev/when email disabled)
}
export interface OIDCClient {
id: string;
name: string;
client_id: string;
redirect_uris: string[];
scopes: string[];
grant_types: string[];
2026-05-19 15:13:51 +00:00
allowed_cors_origins: string[] | null;
is_active: boolean;
created_at: string;
}
export interface OIDCClientWithSecret extends OIDCClient {
client_secret: string;
}
2026-01-16 17:31:25 +10:30
// Policy types
export interface OrgPolicyResponse {
security_policy: {
organization_id: string;
mfa_policy_mode: string;
mfa_grace_period_days: number;
notify_days_before: number;
policy_version: number;
};
}
export interface UpdateOrgPolicyDto {
mfa_policy_mode: string;
mfa_grace_period_days: number;
notify_days_before: number;
}
export interface OrgCompliancePage {
members: OrgComplianceMember[];
count: number;
page: number;
page_size: number;
}
export interface OrgComplianceMember {
user_id: string;
user_email: string;
user_name: string;
status: string;
deadline_at: string | null;
compliant_at: string | null;
last_notified_at: string | null;
}
2026-01-06 15:33:03 +00:00
export { ApiError };
2026-01-16 17:31:25 +10:30
// SSH Key types
export interface SSHKey {
id: string;
user_id: string;
public_key: string;
description: string | null;
key_type: string | null;
fingerprint: string | null;
verified: boolean;
verified_at: string | null;
created_at: string;
updated_at: string;
}
export interface SSHKeysResponse {
keys: SSHKey[];
count: number;
}
export interface SSHChallengeResponse {
challenge_text: string;
validationText: string;
key_id: string;
}
export interface SSHVerifyResponse {
verified: boolean;
}
export interface SSHCertificate {
id: string;
user_id: string;
ssh_key_id: string | null;
certificate: string;
serial: number | null;
key_id: string | null;
cert_type: string;
principals: string[];
valid_after: string;
valid_before: string;
revoked: boolean;
status: string;
created_at: string;
}
export interface SSHSignResponse {
certificate: string;
serial: number;
principals: string[];
valid_after: string;
valid_before: string;
cert_id?: string;
}
export interface CAPermission {
id: string;
ca_id: string;
user_id: string;
user_email: string | null;
permission: 'sign' | 'admin';
created_at: string;
}
export interface OrgCA {
id: string;
organization_id: string | null;
name: string;
description: string | null;
ca_type: 'user' | 'host';
key_type: string;
public_key: string;
fingerprint: string;
is_active: boolean;
/** True when this entry represents the server-wide config-file CA.
* System CAs are read-only — they cannot be edited, deleted, or replaced
* from the UI. */
is_system?: boolean;
default_cert_validity_hours: number;
max_cert_validity_hours: number;
total_certs: number;
active_certs: number;
revoked_certs: number;
/** Next serial number that will be assigned when a certificate is issued. */
next_serial_number: number | null;
created_at: string | null;
updated_at: string | null;
/** Set when the key was last rotated. */
rotated_at: string | null;
/** Reason provided when the key was last rotated. */
rotation_reason: string | null;
}
2026-01-16 17:31:25 +10:30
// Reusable 403 error handler for API calls
// Shows a user-friendly toast message when access is denied
export function create403Handler(toastFn: (options: { title: string; description: string; variant: "destructive" }) => void) {
return (error: ApiError) => {
console.warn('[API] 403 Access Denied:', error.message);
toastFn({
title: "Access Denied",
description: "You don't have permission to view this section. Please contact your organization administrator.",
variant: "destructive",
});
};
}
// ── ZeroTier / Portal Network Types ──────────────────────────────────────────
export type NetworkEnvironment = "production" | "staging" | "development" | "lab";
export type NetworkRequestMode = "open" | "approval_required" | "invite_only";
export type ApprovalGrantType = "requested" | "assigned";
export type ApprovalState = "pending" | "approved" | "rejected" | "revoked" | "suspended";
export type MembershipState =
| "pending_device_registration"
| "pending_request"
| "pending_manager_approval"
| "approved_inactive"
| "joined_deauthorized"
| "active_authorized"
| "activation_expired"
| "suspended"
| "revoked"
| "rejected";
export type ActivationEndReason = "expired" | "logout" | "kill_switch" | "manual_revoke" | "approval_revoked" | "admin_action";
export type KillSwitchScope = "organization" | "global" | "selected_networks";
export type DeviceStatus = "active" | "inactive";
export interface PortalNetwork {
id: string;
organization_id: string;
name: string;
description: string | null;
owner_user_id: string;
zerotier_network_id: string;
environment: NetworkEnvironment;
request_mode: NetworkRequestMode;
default_activation_lifetime_minutes: number;
max_activation_lifetime_minutes: number | null;
is_active: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
approved_user_count?: number;
active_membership_count?: number;
}
2026-03-29 21:33:37 +05:45
/** A ZeroTier network returned from the controller, annotated with whether
* it is already managed as a portal network in Secuird. */
export interface AvailableZtNetwork {
id: string;
name: string;
description: string | null;
owner_id: string | null;
online_member_count: number;
authorized_member_count: number;
total_member_count: number;
already_managed: boolean;
portal_network_id: string | null;
portal_network_name: string | null;
}
export interface OrgMember {
id: string;
user_id: string;
organization_id: string;
role: string;
created_at: string;
updated_at: string;
deleted_at: string | null;
invited_at: string | null;
invited_by_id: string | null;
joined_at: string | null;
user: {
id: string;
email: string;
full_name: string | null;
status: string;
avatar_url: string | null;
activated: boolean;
email_verified: boolean;
created_at: string;
updated_at: string;
last_login_at: string | null;
last_login_ip: string | null;
} | null;
}
export interface Device {
id: string;
user_id: string;
organization_id: string;
node_id: string;
device_nickname: string | null;
hostname: string | null;
asset_tag: string | null;
serial_number: string | null;
status: DeviceStatus;
created_at: string;
updated_at: string;
deleted_at: string | null;
display_name?: string;
active_membership_count?: number;
}
export interface UserNetworkApproval {
id: string;
organization_id: string;
user_id: string;
user_name: string | null;
user_email: string | null;
portal_network_id: string;
device_id?: string;
device_name?: string | null;
device_nickname?: string | null;
active?: boolean;
active_session?: ActivationSession | null;
join_seen?: boolean;
granted_by_user_id: string | null;
grant_type: ApprovalGrantType;
status: ApprovalState;
justification: string | null;
created_at: string;
updated_at: string;
deleted_at: string | null;
active_membership_count?: number;
}
export interface DeviceNetworkMembership {
id: string;
organization_id: string;
user_id: string;
user_name: string | null;
user_email: string | null;
device_id: string;
device_name: string | null;
device_node_id: string | null;
portal_network_id: string;
user_network_approval_id: string | null;
2026-05-28 05:58:56 +00:00
active: boolean;
status: ApprovalState;
grant_type: ApprovalGrantType;
granted_by_user_id: string | null;
justification: string | null;
join_seen: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
active_session: ActivationSession | null;
}
2026-05-28 05:58:56 +00:00
export function deriveMembershipState(status: ApprovalState, active: boolean): MembershipState {
if (active) return "active_authorized";
if (status === "approved") return "approved_inactive";
if (status === "pending") return "pending_manager_approval";
if (status === "rejected") return "rejected";
if (status === "revoked") return "revoked";
if (status === "suspended") return "suspended";
return "pending_manager_approval";
}
export interface EnrichedMembership {
id: string;
user_id: string;
user_email: string | null;
user_full_name: string | null;
device_id: string;
device_nickname: string | null;
device_hostname: string | null;
device_node_id: string | null;
device_status: DeviceStatus | null;
portal_network_id: string;
network_name: string | null;
network_environment: NetworkEnvironment | null;
state: MembershipState | null;
join_seen: boolean;
currently_authorized: boolean;
approved_for_activation: boolean;
user_network_approval_id: string | null;
approval_state: ApprovalState | null;
active_session: ActivationSession | null;
created_at: string | null;
updated_at: string | null;
}
export interface ActivationSession {
id: string;
organization_id: string;
user_id: string;
device_network_membership_id: string;
authenticated_at: string;
expires_at: string;
ended_at: string | null;
end_reason: ActivationEndReason | null;
created_by: string;
created_at: string;
updated_at: string;
deleted_at: string | null;
is_expired: boolean;
is_active: boolean;
}
2026-05-29 06:28:26 +00:00
export interface UserSessionDevice {
id: string;
node_id: string;
name: string;
}
export interface UserSessionNetwork {
id: string;
name: string;
}
export interface UserSession {
id: string;
authenticated_at: string;
expires_at: string;
duration_seconds: number;
remaining_seconds: number;
is_active: boolean;
is_expired: boolean;
ended_at: string | null;
end_reason: string | null;
device: UserSessionDevice;
network: UserSessionNetwork;
}
export interface AdminSession {
id: string;
user: { id: string; full_name: string; email: string };
authenticated_at: string;
expires_at: string;
duration_seconds: number;
remaining_seconds: number;
is_active: boolean;
is_expired: boolean;
ended_at: string | null;
end_reason: string | null;
device: UserSessionDevice;
network: UserSessionNetwork;
}
export interface KillSwitchEvent {
id: string;
organization_id: string;
target_user_id: string;
scope: KillSwitchScope;
triggered_by_user_id: string;
reason: string | null;
network_ids: string[] | null;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
export interface ZeroTierMember {
id: string;
network_id: string;
node_id: string;
name: string | null;
description: string | null;
hidden: boolean;
is_authorized: boolean;
display_name: string;
ip_list: string;
last_online: number | null;
last_seen: number | null;
last_seen_str: string;
client_version: string | null;
controller_id: string | null;
config: {
authorized: boolean;
active_bridge: boolean;
ip_assignments: string[];
creation_time: number | null;
last_authorized_time: number | null;
last_deauthorized_time: number | null;
version_string: string;
};
}
export interface ZeroTierNetwork {
id: string;
name: string;
description: string | null;
owner_id: string | null;
online_member_count: number;
authorized_member_count: number;
total_member_count: number;
config: {
name: string;
private: boolean;
creation_time: number | null;
ip_assignment_pools: Record<string, unknown>[];
routes: Record<string, unknown>[];
};
2026-03-29 21:33:37 +05:45
}
/** Current per-org ZeroTier config as returned by GET /organizations/:id/zerotier-config */
export interface ZeroTierOrgConfig {
/** Whether an API token has been saved (the actual value is never returned). */
zt_api_token_set: boolean;
/** Custom controller / Central base URL, or null when server default is used. */
zt_api_url: string | null;
/** "central" | "controller", or null when server default is used. */
zt_api_mode: "central" | "controller" | null;
}
/** Body for PUT /organizations/:id/zerotier-config */
export interface ZeroTierOrgConfigInput {
zt_api_token: string;
zt_api_url: string;
zt_api_mode: "central" | "controller";
2026-01-16 17:31:25 +10:30
}