diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..5d1e54d --- /dev/null +++ b/frontend/src/api/auth.js @@ -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') +} diff --git a/frontend/src/api/documents.js b/frontend/src/api/documents.js new file mode 100644 index 0000000..7226058 --- /dev/null +++ b/frontend/src/api/documents.js @@ -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} — 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 +} diff --git a/frontend/src/api/topics.js b/frontend/src/api/topics.js new file mode 100644 index 0000000..6996656 --- /dev/null +++ b/frontend/src/api/topics.js @@ -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 }), + }) +}