From a895b1812f25d90a071ae71cbaff91b19b686976 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Wed, 10 Jun 2026 18:42:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(08-07):=20create=20domain=20modules=20admi?= =?UTF-8?q?n.js,=20folders.js,=20shares.js,=20cloud.js=20=E2=80=94=20CODE-?= =?UTF-8?q?04?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin.js: 17 functions; adminExportAuditLogCsv + adminDownloadDailyExport use fetchWithRetry - folders.js: 6 functions — import request from utils.js - shares.js: 5 functions — import request from utils.js - cloud.js: 7 functions including initiateOAuth — import request from utils.js - Blob-download retry boilerplate eliminated from admin.js (CODE-08) --- frontend/src/api/admin.js | 167 ++++++++++++++++++++++++++++++++++++ frontend/src/api/cloud.js | 61 +++++++++++++ frontend/src/api/folders.js | 46 ++++++++++ frontend/src/api/shares.js | 36 ++++++++ 4 files changed, 310 insertions(+) create mode 100644 frontend/src/api/admin.js create mode 100644 frontend/src/api/cloud.js create mode 100644 frontend/src/api/folders.js create mode 100644 frontend/src/api/shares.js diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js new file mode 100644 index 0000000..1236295 --- /dev/null +++ b/frontend/src/api/admin.js @@ -0,0 +1,167 @@ +/** + * Admin API — user management, quota, AI config, audit log, daily exports. + * + * Consumers: AdminUsersTab.vue, AdminQuotasTab.vue, AdminAiConfigTab.vue, + * AuditLogTab.vue, AdminDailyExportsTab.vue + */ + +import { request, fetchWithRetry } from './utils.js' + +export function adminListUsers() { + return request('/api/admin/users') +} + +export function adminCreateUser(body) { + return request('/api/admin/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) +} + +export function adminDeactivateUser(id) { + return request(`/api/admin/users/${id}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_active: false }), + }) +} + +export function adminReactivateUser(id) { + return request(`/api/admin/users/${id}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_active: true }), + }) +} + +export function adminResetUserPassword(id) { + return request(`/api/admin/users/${id}/password-reset`, { method: 'POST' }) +} + +export function adminGetUserQuota(id) { + return request(`/api/admin/users/${id}/quota`) +} + +export function adminUpdateQuota(id, limitBytes) { + return request(`/api/admin/users/${id}/quota`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ limit_bytes: limitBytes }), + }) +} + +export function adminUpdateAiConfig(id, provider, model) { + return request(`/api/admin/users/${id}/ai-config`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ai_provider: provider, ai_model: model }), + }) +} + +export function adminDeleteUser(id, adminPassword) { + return request(`/api/admin/users/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ admin_password: adminPassword }), + }) +} + +export function getAiConfig() { + return request('/api/admin/ai-config', { method: 'GET' }) +} + +export function saveAiConfig(body) { + return request('/api/admin/ai-config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) +} + +export function testAiConnection(providerId, overrides = {}) { + return request('/api/admin/ai-config/test-connection', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ provider_id: providerId, ...overrides }), + }) +} + +export function getAiModels(providerId) { + return request( + '/api/admin/ai-config/models?provider_id=' + encodeURIComponent(providerId), + { method: 'GET' } + ) +} + +export function adminListAuditLog({ start, end, user_handle, event_type, page = 1, per_page = 50 } = {}) { + const params = new URLSearchParams() + if (start) params.set('start', start) + if (end) params.set('end', end) + if (user_handle) params.set('user_handle', user_handle) + if (event_type) params.set('event_type', event_type) + params.set('page', page) + params.set('per_page', per_page) + return request(`/api/admin/audit-log?${params}`) +} + +/** + * Export the audit log as a CSV file using fetch + Blob URL. + * + * Unlike window.location.href, this sends the Authorization Bearer header so + * the endpoint can authenticate the request (D-13, T-06.2-04-03). + * + * Refactored to use fetchWithRetry() — the retry boilerplate is consolidated + * in utils.js (CODE-08). + * + * Must NOT call res.json() — CSV is text/csv. + */ +export async function adminExportAuditLogCsv(params = {}) { + const searchParams = new URLSearchParams({ format: 'csv' }) + if (params.start) searchParams.set('start', params.start) + if (params.end) searchParams.set('end', params.end) + if (params.user_handle) searchParams.set('user_handle', params.user_handle) + if (params.event_type) searchParams.set('event_type', params.event_type) + + const res = await fetchWithRetry(`/api/admin/audit-log/export?${searchParams}`) + if (!res.ok) throw new Error(`Export failed: ${res.status}`) + + const text = await res.text() + const blob = new Blob([text], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'audit-export.csv' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + setTimeout(() => URL.revokeObjectURL(url), 1000) +} + +export function adminListDailyExports() { + return request('/api/admin/audit-log/daily-exports') +} + +/** + * Download a specific Celery daily audit export file from MinIO using fetch + Blob URL. + * + * Uses fetchWithRetry() to send the Authorization Bearer header (D-17, T-06.2-04-03). + * Refactored to use fetchWithRetry() — retry logic consolidated in utils.js (CODE-08). + * + * @param {string} date — YYYY-MM-DD format date string + */ +export async function adminDownloadDailyExport(date) { + const res = await fetchWithRetry(`/api/admin/audit-log/daily-exports/${date}`) + if (!res.ok) throw new Error(`Download failed: ${res.status}`) + + const text = await res.text() + const blob = new Blob([text], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `audit-${date}.csv` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + setTimeout(() => URL.revokeObjectURL(url), 1000) +} diff --git a/frontend/src/api/cloud.js b/frontend/src/api/cloud.js new file mode 100644 index 0000000..1fc19de --- /dev/null +++ b/frontend/src/api/cloud.js @@ -0,0 +1,61 @@ +/** + * Cloud Storage API — connections, OAuth initiation, folder browsing, config. + * + * Consumers: stores/cloudConnections.js, SettingsCloudTab.vue, + * CloudCredentialModal.vue, CloudProviderTreeItem.vue, + * CloudFolderTreeItem.vue + */ + +import { request } from './utils.js' + +export function listCloudConnections() { + return request('/api/cloud/connections') +} + +export function disconnectCloud(id) { + return request(`/api/cloud/connections/${id}`, { method: 'DELETE' }) +} + +export function connectWebDav(provider, serverUrl, username, password) { + return request('/api/cloud/connections/webdav', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ provider, server_url: serverUrl, username, password }), + }) +} + +export function updateDefaultStorage(backend) { + return request('/api/users/me/default-storage', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ backend }), + }) +} + +export function getCloudFolders(provider, folderId) { + return request(`/api/cloud/folders/${provider}/${folderId}`) +} + +/** + * Initiate OAuth flow for Google Drive or OneDrive. + * + * Returns a JSON object {url: ""} from the backend. + * The caller is responsible for navigating: window.location.href = data.url + * + * Using request() (not bare window.location.href) ensures the Bearer header + * is injected and the 401→refresh retry path fires if the token has expired. + * See plan 05-10 trust boundary: frontend→/api/cloud/oauth/initiate/{provider}. + */ +export function initiateOAuth(provider) { + return request(`/api/cloud/oauth/initiate/${provider}`) +} + +/** + * Fetch non-secret configuration for a WebDAV/Nextcloud connection (edit flow). + * + * Returns {id, provider, server_url, connection_username} — never the password. + * Used to pre-populate the Edit modal when re-editing an existing connection. + */ +export function getConnectionConfig(connectionId) { + return request(`/api/cloud/connections/${connectionId}/config`) +} diff --git a/frontend/src/api/folders.js b/frontend/src/api/folders.js new file mode 100644 index 0000000..7f8906b --- /dev/null +++ b/frontend/src/api/folders.js @@ -0,0 +1,46 @@ +/** + * Folders API — listing, creation, renaming, deletion, document moves. + * + * Consumers: stores/folders.js, FolderTreeItem.vue, AppSidebar.vue + */ + +import { request } from './utils.js' + +export function listFolders(parentId = null) { + const params = new URLSearchParams() + if (parentId != null) params.set('parent_id', parentId) + const qs = params.toString() + return request(`/api/folders${qs ? `?${qs}` : ''}`) +} + +export function createFolder(name, parentId = null) { + return request('/api/folders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, parent_id: parentId || null }), + }) +} + +export function getFolder(folderId) { + return request(`/api/folders/${folderId}`) +} + +export function renameFolder(folderId, name) { + return request(`/api/folders/${folderId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }) +} + +export function deleteFolder(folderId) { + return request(`/api/folders/${folderId}`, { method: 'DELETE' }) +} + +export function moveDocument(docId, folderId) { + return request(`/api/documents/${docId}/folder`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ folder_id: folderId || null }), + }) +} diff --git a/frontend/src/api/shares.js b/frontend/src/api/shares.js new file mode 100644 index 0000000..14a3d42 --- /dev/null +++ b/frontend/src/api/shares.js @@ -0,0 +1,36 @@ +/** + * Shares API — creating, updating, listing, deleting shares and viewing received shares. + * + * Consumers: SharedView.vue, ShareModal.vue + */ + +import { request } from './utils.js' + +export function createShare(docId, recipientHandle, permission = 'view') { + return request('/api/shares', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ document_id: docId, recipient_handle: recipientHandle, permission }), + }) +} + +export function updateSharePermission(shareId, permission) { + return request(`/api/shares/${shareId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ permission }), + }) +} + +export function listShares(docId) { + const params = new URLSearchParams({ document_id: docId }) + return request(`/api/shares?${params}`) +} + +export function deleteShare(shareId) { + return request(`/api/shares/${shareId}`, { method: 'DELETE' }) +} + +export function getSharedWithMe() { + return request('/api/shares/received') +}