feat(08-07): create domain modules documents.js, auth.js, topics.js — CODE-04
- documents.js: 10 functions; fetchDocumentContent refactored to use fetchWithRetry - auth.js: 15 functions (login through getMyQuota) — import request from utils.js - topics.js: 5 functions — import request from utils.js - All functions copied verbatim from client.js; no consumer files modified
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Auth API — login, register, token lifecycle, TOTP, password reset, preferences, quota.
|
||||||
|
*
|
||||||
|
* Consumers: stores/auth.js, SettingsAccountTab.vue, LoginView.vue,
|
||||||
|
* TotpEnrollment.vue, PasswordResetView.vue
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { request } from './utils.js'
|
||||||
|
|
||||||
|
export function login(body) {
|
||||||
|
return request('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(body) {
|
||||||
|
return request('/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshToken() {
|
||||||
|
// No body — httpOnly cookie sent automatically via credentials: 'include'
|
||||||
|
return request('/api/auth/refresh', { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return request('/api/auth/logout', { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logoutAll() {
|
||||||
|
return request('/api/auth/logout-all', { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMe() {
|
||||||
|
return request('/api/auth/me')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePassword(body) {
|
||||||
|
return request('/api/auth/change-password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totpSetup() {
|
||||||
|
return request('/api/auth/totp/setup')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totpEnable(code) {
|
||||||
|
return request('/api/auth/totp/enable', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ code }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function totpDisable() {
|
||||||
|
return request('/api/auth/totp', { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function passwordResetRequest(email) {
|
||||||
|
return request('/api/auth/password-reset', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function passwordResetConfirm(token, newPassword) {
|
||||||
|
return request('/api/auth/password-reset/confirm', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ token, new_password: newPassword }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMyPreferences() {
|
||||||
|
return request('/api/auth/me/preferences')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMyPreferences(payload) {
|
||||||
|
return request('/api/auth/me/preferences', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMyQuota() {
|
||||||
|
return request('/api/auth/me/quota')
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Document API — listing, fetching, uploading, content streaming.
|
||||||
|
*
|
||||||
|
* Consumers: stores/documents.js, DocumentCard.vue, DocumentView.vue,
|
||||||
|
* DocumentPreviewModal.vue, FileManagerView.vue, CloudFolderView.vue
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { request, fetchWithRetry } from './utils.js'
|
||||||
|
|
||||||
|
export function listDocuments({ topic, page = 1, perPage = 20, folderId = null, q = null, sort = null, order = null } = {}) {
|
||||||
|
const params = new URLSearchParams({ page, per_page: perPage })
|
||||||
|
if (topic) params.set('topic', topic)
|
||||||
|
if (folderId != null) params.set('folder_id', folderId)
|
||||||
|
if (q) params.set('q', q)
|
||||||
|
if (sort) params.set('sort', sort)
|
||||||
|
if (order) params.set('order', order)
|
||||||
|
return request(`/api/documents?${params}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocument(id) {
|
||||||
|
return request(`/api/documents/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocument(id, removeOnly = false) {
|
||||||
|
const url = removeOnly ? `/api/documents/${id}?remove_only=true` : `/api/documents/${id}`
|
||||||
|
return request(url, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocumentRemoveOnly(id) {
|
||||||
|
return deleteDocument(id, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function classifyDocument(id, topics = null) {
|
||||||
|
return request(`/api/documents/${id}/classify`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(topics ? { topics } : {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUploadUrl(filename, contentType) {
|
||||||
|
return request('/api/documents/upload-url', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ filename, content_type: contentType }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmUpload(documentId) {
|
||||||
|
return request(`/api/documents/${documentId}/confirm`, { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadToCloud(file, provider, folderPath) {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('file', file)
|
||||||
|
form.append('target_backend', provider)
|
||||||
|
if (folderPath) form.append('cloud_folder_path', folderPath)
|
||||||
|
return request('/api/documents/upload', { method: 'POST', body: form })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocumentContentUrl(docId) {
|
||||||
|
return `/api/documents/${docId}/content`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch document content bytes with authentication, returning the raw Response.
|
||||||
|
*
|
||||||
|
* Refactored to use fetchWithRetry() which consolidates the auth-injection +
|
||||||
|
* 401-retry boilerplate that was previously copy-pasted here (CODE-08).
|
||||||
|
*
|
||||||
|
* Unlike request(), this does NOT call res.json() — returns raw Response so
|
||||||
|
* callers can call .blob() to build an object URL for iframe preview or
|
||||||
|
* window.open() without an unauthenticated src= attribute.
|
||||||
|
*
|
||||||
|
* Security: closes the unauthenticated content-access gap where an iframe src=
|
||||||
|
* or window.open() with a raw /content URL would bypass the Bearer auth check.
|
||||||
|
* See plan 05-09 trust boundary: frontend→/api/documents/{id}/content.
|
||||||
|
*
|
||||||
|
* @param {string} docId
|
||||||
|
* @param {RequestInit} [options]
|
||||||
|
* @returns {Promise<Response>} — raw Response; throws on non-ok status
|
||||||
|
*/
|
||||||
|
export async function fetchDocumentContent(docId, options = {}) {
|
||||||
|
const res = await fetchWithRetry(`/api/documents/${docId}/content`, options)
|
||||||
|
if (!res.ok) throw new Error(`Failed to fetch document content: ${res.status}`)
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Topics API — listing, creation, update, deletion, AI suggestion.
|
||||||
|
*
|
||||||
|
* Consumers: stores/topics.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { request } from './utils.js'
|
||||||
|
|
||||||
|
export function listTopics() {
|
||||||
|
return request('/api/topics')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTopic({ name, description = '', color = '#6366f1' }) {
|
||||||
|
return request('/api/topics', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, description, color }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTopic(id, patch) {
|
||||||
|
return request(`/api/topics/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTopic(id) {
|
||||||
|
return request(`/api/topics/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suggestTopics(documentId) {
|
||||||
|
return request('/api/topics/suggest', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ document_id: documentId }),
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user