feat(08-07): create domain modules admin.js, folders.js, shares.js, cloud.js — CODE-04
- 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)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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: "<authorization_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`)
|
||||
}
|
||||
@@ -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 }),
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
Reference in New Issue
Block a user