feat: 2FA/TOTP, config templates (v0.11.4 Beta)

- 2FA: TOTP setup wizard in Settings, QR code enrollment, login second-step, password-confirmed disable
- Config templates: reusable DNS/NTP/syslog config sets pushed to selected devices via RouterOS API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2GT-Rich
2026-05-16 15:44:23 -07:00
co-authored by Claude Sonnet 4.6
parent 49ab1b002e
commit a1ca4d0a44
12 changed files with 1036 additions and 78 deletions
+38 -1
View File
@@ -43,10 +43,20 @@ api.interceptors.response.use(
// ─── Auth ─────────────────────────────────────────────────────────────────────
export const authApi = {
login: (username: string, password: string) =>
api.post<{ token: string; user: import('../types').User }>(
api.post<{ token?: string; user?: import('../types').User; requires_totp?: boolean; totp_token?: string }>(
'/auth/login',
{ username, password }
),
totpVerify: (totp_token: string, code: string) =>
api.post<{ token: string; user: import('../types').User }>('/auth/totp/verify', { totp_token, code }),
totpSetup: () =>
api.post<{ secret: string; uri: string; qr: string }>('/auth/totp/setup'),
totpConfirm: (code: string) =>
api.post<{ ok: boolean }>('/auth/totp/confirm', { code }),
totpDisable: (password: string) =>
api.post<{ ok: boolean }>('/auth/totp/disable', { password }),
totpStatus: () =>
api.get<{ totp_enabled: boolean }>('/auth/totp/status'),
me: () => api.get('/auth/me'),
changePassword: (currentPassword: string, newPassword: string) =>
api.put('/auth/password', { currentPassword, newPassword }),
@@ -776,4 +786,31 @@ export const auditLogApi = {
}>('/audit-log', { params }),
};
export interface ConfigTemplate {
id: number;
name: string;
description: string | null;
applies_to_type: string | null;
template_json: {
dns_servers?: string[];
ntp_servers?: string[];
syslog_host?: string;
};
created_at: string;
updated_at: string;
}
export const configTemplatesApi = {
list: () => api.get<ConfigTemplate[]>('/config-templates'),
get: (id: number) => api.get<ConfigTemplate>(`/config-templates/${id}`),
create: (data: Partial<ConfigTemplate>) => api.post<ConfigTemplate>('/config-templates', data),
update: (id: number, data: Partial<ConfigTemplate>) => api.put<ConfigTemplate>(`/config-templates/${id}`, data),
delete: (id: number) => api.delete(`/config-templates/${id}`),
apply: (id: number, device_ids: number[]) =>
api.post<{ results: { device_id: number; device_name: string; ok: boolean; error?: string }[] }>(
`/config-templates/${id}/apply`,
{ device_ids }
),
};
export default api;