296 lines
9.6 KiB
TypeScript
296 lines
9.6 KiB
TypeScript
// Client HTTP vers l'API Omnex. "" (chemin relatif) par défaut : same-origin
|
|
// en prod (nginx proxy /api/ vers l'API, voir docker/waf/nginx.conf) et en
|
|
// dev (proxy Vite, voir vite.config.ts) — nécessaire pour que le cookie de
|
|
// session SameSite=Lax soit envoyé (jamais cross-origin, voir lib/auth.tsx).
|
|
const BASE = import.meta.env.VITE_API_URL ?? ''
|
|
|
|
export class ApiError extends Error {
|
|
status: number
|
|
constructor(status: number, message: string) {
|
|
super(message)
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
// Authentification exclusivement via le cookie de session HttpOnly (posé par
|
|
// le backend sur /auth/login et /auth/register) — jamais de JWT lu/stocké en
|
|
// JS. Le dupliquer en localStorage (comme avant) annulait la protection
|
|
// HttpOnly contre le vol de session par XSS (pentest F-003) : le SPA et
|
|
// l'API étant same-origin, le cookie authentifie déjà chaque fetch() sans
|
|
// qu'aucun en-tête Authorization manuel ne soit nécessaire.
|
|
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
|
|
const res = await fetch(`${BASE}/api/v1${path}`, {
|
|
method,
|
|
headers,
|
|
credentials: 'include',
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
})
|
|
if (!res.ok) {
|
|
const msg = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
throw new ApiError(res.status, msg.error ?? `HTTP ${res.status}`)
|
|
}
|
|
return res.status === 204 ? (undefined as T) : ((await res.json()) as T)
|
|
}
|
|
|
|
// --- Types ---
|
|
|
|
export type Role = 'admin' | 'client'
|
|
|
|
export type DemoStatus =
|
|
| 'pending'
|
|
| 'provisioning'
|
|
| 'ready'
|
|
| 'expiring'
|
|
| 'expired'
|
|
| 'failed'
|
|
|
|
export interface Demo {
|
|
id: string
|
|
username: string
|
|
status: DemoStatus
|
|
namespace: string
|
|
url: string
|
|
custom_domain?: string
|
|
type_abonnement?: string
|
|
created_at: string
|
|
expires_at: string
|
|
}
|
|
|
|
// Projets vitrine : abonnement de 1 à 12 mois, supprimé à l'échéance
|
|
// (voir internal/projects côté API).
|
|
export type ProjectStatus =
|
|
| 'pending'
|
|
| 'provisioning'
|
|
| 'ready'
|
|
| 'deleting'
|
|
| 'deleted'
|
|
| 'failed'
|
|
|
|
export interface Project {
|
|
id: string
|
|
client_name: string
|
|
status: ProjectStatus
|
|
namespace: string
|
|
host: string
|
|
url: string
|
|
admin_username: string
|
|
admin_number: number
|
|
months_purchased: number
|
|
created_at: string
|
|
expires_at: string
|
|
}
|
|
|
|
export interface CreateProjectParams {
|
|
clientName: string
|
|
months: number
|
|
adminUsername: string
|
|
adminPassword: string
|
|
adminNumber: number
|
|
}
|
|
|
|
export type StorageDriver = 'local' | 's3'
|
|
|
|
export interface CreateDemoParams {
|
|
username: string
|
|
telegramBotUsername?: string
|
|
telegramBotToken?: string
|
|
nowPaymentsApiKey?: string
|
|
nowPaymentsIpnSecret?: string
|
|
storageDriver: StorageDriver
|
|
s3Bucket?: string
|
|
s3Endpoint?: string
|
|
tomtomApiKey?: string
|
|
tomtomApiKey1?: string
|
|
tomtomApiKey2?: string
|
|
tomtomApiKey3?: string
|
|
lbBot1Username?: string
|
|
lbBot1Token?: string
|
|
lbBot2Username?: string
|
|
lbBot2Token?: string
|
|
lbStrategy?: 'failover' | 'roundrobin' | 'leastconn'
|
|
lbJwtTtlSeconds?: string
|
|
lbHealthCheckInterval?: string
|
|
adminUsername: string
|
|
adminPassword: string
|
|
}
|
|
|
|
export interface CodeBuySub {
|
|
id: string
|
|
username: string
|
|
code_verif: string
|
|
created_at: string
|
|
}
|
|
|
|
export interface PremiumUser {
|
|
username: string
|
|
activated_at: string
|
|
expired_at: string
|
|
}
|
|
|
|
export interface Contact {
|
|
id: string
|
|
username: string
|
|
telegram: string
|
|
sujet: string
|
|
message: string
|
|
created_at: string
|
|
}
|
|
|
|
export interface ComponentState {
|
|
phase: string
|
|
cpu_milli: number
|
|
cpu_limit_milli: number
|
|
memory_mi: number
|
|
memory_limit_mi: number
|
|
}
|
|
|
|
export interface DemoState {
|
|
api: ComponentState
|
|
web: ComponentState
|
|
db: ComponentState
|
|
dbm: ComponentState
|
|
// lb (load-balancer Telegram) est optionnel : phase reste "" si le chart
|
|
// lbtelegram n'est pas installé pour cette démo.
|
|
lb: ComponentState
|
|
}
|
|
|
|
export interface DemoDetails {
|
|
namespace: string
|
|
state: DemoState
|
|
}
|
|
|
|
export interface UpdateUsername {
|
|
username: string
|
|
}
|
|
|
|
export interface UpdatePassword {
|
|
password: string
|
|
}
|
|
|
|
export interface TelegramInfo {
|
|
telegram: string
|
|
}
|
|
|
|
export interface AlertSettings {
|
|
discord_webhook_url: string
|
|
telegram_bot_token: string
|
|
telegram_chat_id: string
|
|
}
|
|
|
|
export interface AlertChannelResult {
|
|
ok: boolean
|
|
error?: string
|
|
}
|
|
|
|
export interface TestAlertSettingsResponse {
|
|
discord?: AlertChannelResult
|
|
telegram?: AlertChannelResult
|
|
}
|
|
|
|
export interface AppDownload {
|
|
name: string
|
|
size_bytes: number
|
|
}
|
|
|
|
export interface AppDownloadsResponse {
|
|
eligible: boolean
|
|
items: AppDownload[]
|
|
}
|
|
|
|
// --- Endpoints ---
|
|
|
|
export const api = {
|
|
login: (username: string, password: string, role: Role) =>
|
|
request<{ role: Role }>('POST', '/auth/login', { username, password, role }),
|
|
register: (username: string, password: string) =>
|
|
request<{ role: Role }>('POST', '/auth/register', { username, password }),
|
|
me: () =>
|
|
request<{
|
|
user_id: string
|
|
username: string
|
|
role: Role
|
|
type_abonnement: string
|
|
expired_at: string
|
|
}>('GET', '/auth/me'), logout: () => request<{ status: string }>('POST', '/auth/logout'),
|
|
|
|
listDemos: () => request<{ items: Demo[] }>('GET', '/demos'),
|
|
listMyDemos: () => request<{ items: Demo[] }>('GET', '/demos/mine'),
|
|
getDemo: (id: string) => request<Demo>('GET', `/demos/${id}`),
|
|
createDemo: (params: CreateDemoParams) =>
|
|
request<Demo>('POST', '/demos', {
|
|
username: params.username,
|
|
...(params.telegramBotUsername ? { telegram_bot_username: params.telegramBotUsername } : {}),
|
|
...(params.telegramBotToken ? { telegram_bot_token: params.telegramBotToken } : {}),
|
|
...(params.nowPaymentsApiKey ? { nowpayments_api_key: params.nowPaymentsApiKey } : {}),
|
|
...(params.nowPaymentsIpnSecret ? { nowpayments_ipn_secret: params.nowPaymentsIpnSecret } : {}),
|
|
storage_driver: params.storageDriver,
|
|
...(params.storageDriver === 's3'
|
|
? { s3_bucket: params.s3Bucket, s3_endpoint: params.s3Endpoint }
|
|
: {}),
|
|
...(params.tomtomApiKey ? { tomtom_api_key: params.tomtomApiKey } : {}),
|
|
...(params.tomtomApiKey1 ? { tomtom_api_key_1: params.tomtomApiKey1 } : {}),
|
|
...(params.tomtomApiKey2 ? { tomtom_api_key_2: params.tomtomApiKey2 } : {}),
|
|
...(params.tomtomApiKey3 ? { tomtom_api_key_3: params.tomtomApiKey3 } : {}),
|
|
...(params.lbBot1Username ? { lb_bot1_username: params.lbBot1Username, lb_bot1_token: params.lbBot1Token } : {}),
|
|
...(params.lbBot2Username ? { lb_bot2_username: params.lbBot2Username, lb_bot2_token: params.lbBot2Token } : {}),
|
|
...(params.lbStrategy ? { lb_strategy: params.lbStrategy } : {}),
|
|
...(params.lbJwtTtlSeconds ? { lb_jwt_ttl_seconds: params.lbJwtTtlSeconds } : {}),
|
|
...(params.lbHealthCheckInterval ? { lb_health_check_interval: params.lbHealthCheckInterval } : {}),
|
|
admin_username: params.adminUsername,
|
|
admin_password: params.adminPassword,
|
|
}),
|
|
extendDemo: (id: string) => request<Demo>('POST', `/demos/${id}/extend`),
|
|
deleteDemo: (id: string) => request<Demo>('DELETE', `/demos/${id}`),
|
|
setDemoDomain: (id: string, domain: string) =>
|
|
request<Demo>('POST', `/demos/${id}/domain`, { domain }),
|
|
transferDemoToPremium: (id: string) => request<Demo>('POST', `/demos/${id}/premium`),
|
|
|
|
listProjects: () => request<{ items: Project[] }>('GET', '/projects'),
|
|
createProject: (params: CreateProjectParams) =>
|
|
request<Project>('POST', '/projects', {
|
|
client_name: params.clientName,
|
|
months: params.months,
|
|
admin_username: params.adminUsername,
|
|
admin_password: params.adminPassword,
|
|
admin_number: params.adminNumber,
|
|
}),
|
|
extendProject: (id: string, months: number) =>
|
|
request<Project>('POST', `/projects/${id}/extend`, { months }),
|
|
deleteProject: (id: string) => request<Project>('DELETE', `/projects/${id}`),
|
|
|
|
listCodes: () => request<{ items: CodeBuySub[] }>('GET', '/codes'),
|
|
listPremiumUsers: () => request<{ items: PremiumUser[] }>('GET', '/premium'),
|
|
createCode: (username: string) =>
|
|
request<{ code: string }>('POST', '/codes', { username }),
|
|
addCode: (code: string) =>
|
|
request<{ success: string }>('POST', '/subscription', { code_verif: code }),
|
|
sendMessage: (username: string, telegram: string, sujet: string, message: string) =>
|
|
request<{ success: string }>('POST', '/send/message', { username, telegram, sujet, message }),
|
|
getMessage: () => request<{ messages: Contact[] }>('GET', '/messages'),
|
|
getDemoDetails: (namespace: string) =>
|
|
request<DemoDetails>('POST', '/demos/details', { namespace }),
|
|
updateUsername: (username: string) =>
|
|
request<UpdateUsername>('POST', '/profile/username', { username }),
|
|
updatePassword: (password: string) =>
|
|
request<UpdatePassword>('POST', '/profile/password', { password }),
|
|
getTelegram: () => request<TelegramInfo>('GET', '/profile/telegram'),
|
|
setTelegram: (telegram: string) =>
|
|
request<TelegramInfo>('POST', '/profile/telegram', { telegram }),
|
|
getAlertSettings: () => request<AlertSettings>('GET', '/profile/alerts'),
|
|
setAlertSettings: (settings: AlertSettings) =>
|
|
request<AlertSettings>('POST', '/profile/alerts', settings),
|
|
testAlertSettings: (settings: AlertSettings) =>
|
|
request<TestAlertSettingsResponse>('POST', '/profile/alerts/test', settings),
|
|
|
|
listAppDownloads: () => request<AppDownloadsResponse>('GET', '/apps'),
|
|
}
|
|
|
|
// URL de téléchargement direct d'une app — le cookie de session httpOnly
|
|
// suffit à authentifier la navigation (voir auth.tokenFromRequest côté API),
|
|
// pas besoin de fetch + blob.
|
|
export function appDownloadUrl(name: string): string {
|
|
return `${BASE}/api/v1/apps/${encodeURIComponent(name)}`
|
|
}
|