import { addUpload } from "../../operations/addUpload.js"; import { addRedirect } from "../../operations/addRedirect.js"; import { removeUpload } from "../../operations/removeUpload.js"; import { removeRedirect } from "../../operations/removeRedirect.js"; import { updateUpload } from "../../operations/updateUpload.js"; import { updateViaArchive } from "../../operations/updateViaArchive.js"; import { getDocumentClientState } from "../../operations/getDocumentClientState.js"; import { functionContext } from "../../fn/functionContext.js"; import { htmlEditorPage } from "../utils/htmlEditorPage.js"; import { htmlLoginPage } from "../utils/htmlLoginPage.js"; import { signup } from "../../operations/signup.js"; import { deleteSession } from "../../operations/deleteSession.js"; import { login } from "../../operations/login.js"; import { createSession } from "../../operations/createSession.js"; import { getUsersCount } from "../../operations/getUsersCount.js"; import { search } from "../../operations/search.js"; import { removeDocument } from "../../operations/removeDocument.js"; import { Readable } from 'stream'; // GET: /edit?whoami - validate auth and return OK export const whoami = async (c, _next) => { return c.text('OK', 200); }; // Middleware to check if path ends with /edit and extract docPath export const editPage = async (c, next) => { const fullPath = c.req.path; if (!fullPath.endsWith('/edit')) { return next(); } const docPath = fullPath.slice(0, -5) || '/'; c.set('docPath', docPath); await next(); }; // Middleware to return 404 for non-edit paths when document was not found // This runs after documentHandler and editPage, so if we reach here without docPath, // it means: 1) no document was found, 2) path doesn't end with /edit // Exception: .git paths should be handled by gitHandler later export const notFoundPage = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { // No document found and not an edit page - return 404 return c.html('

404 - Not Found

', 404); } return next(); }; // POST: Logout export const logoutPost = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const sessionId = c.req.header('cookie')?.match(/session_id=([^;]+)/)?.[1]; if (sessionId) { await deleteSession(c.get('client'), { session_id: sessionId }); } c.header('Set-Cookie', 'session_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax'); const redirectPath = docPath === '/' ? '/edit' : `${docPath}/edit`; return c.redirect(redirectPath, 302); }; // Middleware to compute and set signup state in context export const signupStateMiddleware = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const allowSignupEnv = process.env.ALLOW_SIGNUP === 'true'; const signupLimitEnv = process.env.SIGNUP_LIMIT; const signupLimit = signupLimitEnv ? parseInt(signupLimitEnv, 10) : 1; const validLimit = !isNaN(signupLimit) && signupLimit > 0 ? signupLimit : 1; // Determine if signup is actually allowed let allowSignup = allowSignupEnv; if (allowSignup) { const result = await getUsersCount(c.get('client')); allowSignup = result.count < validLimit; } c.set('allowSignup', allowSignup); return next(); }; // GET: If unauthenticated, render login page with 401 (no redirect) export const loginPage = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } if (c.get('isAuthenticated')) { return next(); } const allowSignup = c.get('allowSignup') ?? false; return htmlLoginPage({ allowSignup, status: 401 }); }; // POST: Handle login/signup form submission on /edit (no /edit/login) export const loginPost = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const contentType = c.req.header('content-type') || ''; if (!contentType.includes('application/x-www-form-urlencoded')) { return next(); } const allowSignup = c.get('allowSignup') ?? false; try { const body = await c.req.parseBody(); const action = body.action?.toString(); // 'login' or 'signup' const username = body.username?.toString(); const password = body.password?.toString(); if (!username || !password) { return htmlLoginPage({ error: 'Username and password are required', allowSignup, status: 401 }); } const user = action === 'signup' ? await (async () => { if (!allowSignup) { throw new Error('Signup is disabled'); } const password_confirm = body.password_confirm?.toString(); if (!password_confirm) { throw new Error('Password confirmation is required'); } return signup(c.get('client'), { username, password, password_confirm }); })() : await login(c.get('client'), { username, password }); const session = await createSession(c.get('client'), { user_id: user.id }); // Set the session cookie without SameSite restriction for better compatibility // This allows the cookie to be sent on redirects c.header('Set-Cookie', `session_id=${session.session_id}; HttpOnly; Path=/; Max-Age=${30 * 24 * 60 * 60}`); // Redirect to the editor page after successful login // This ensures the cookie is properly set and the URL is clean const redirectPath = docPath === '/' ? '/edit' : `${docPath}/edit`; return c.redirect(redirectPath, 302); } catch (error) { return htmlLoginPage({ error: error instanceof Error ? error.message : 'Authentication failed', allowSignup, status: 401, }); } }; // POST: Search documents (used by template/slot picker UI) export const searchDocuments = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const query = c.req.query('query') || ''; const limitStr = c.req.query('limit'); // Parse limit (default to 10, max 100) let limit = 10; if (limitStr) { const parsedLimit = parseInt(limitStr, 10); if (isNaN(parsedLimit) || parsedLimit < 1) { return c.json({ error: 'Limit must be a positive integer' }, 400); } limit = Math.min(parsedLimit, 100); } try { const results = await search(c.get('client'), { query, limit, }); return c.json(results); } catch (error) { console.error('Search error:', error); return c.json({ error: error instanceof Error ? error.message : 'Search failed' }, 500); } }; // GET: Render editor page export const renderEditorPage = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const state = await getDocumentClientState(c.get('client'), { path: docPath }, undefined, c.req.query()); // If document doesn't exist, still render editor for new document creation // The editor will create the document on first save // Check if document was accessed via redirect path - redirect to canonical edit path if (state?.document?.redirect) { const canonicalEditPath = state.document.path === '/' ? '/edit' : `${state.document.path}/edit`; return new Response(null, { status: 302, headers: { Location: canonicalEditPath }, }); } return htmlEditorPage({ state, fallbackPath: docPath }); }; // POST: Upload file export const uploadFile = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const contentType = c.req.header('content-type'); if (!contentType?.includes('multipart/form-data')) { return c.json({ error: 'Upload requests must use multipart/form-data' }, 415); } try { const formData = await c.req.formData(); const file = formData.get('file'); if (!file || !(file instanceof File)) { return c.json({ error: 'No file provided' }, 400); } const uploadsPath = c.get('uploadsPath'); const result = await addUpload(c.get('client'), { path: docPath }, uploadsPath, file); return c.json({ success: true, filename: result.filename, original_filename: result.original_filename, hidden: result.hidden, url: `./uploads/${encodeURIComponent(result.original_filename)}`, }); } catch (error) { console.error('Upload error:', error); return c.json({ error: error instanceof Error ? error.message : 'Upload failed' }, 500); } }; // POST: Add redirect export const addRedirectHandler = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const contentType = c.req.header('content-type') || ''; if (!contentType.includes('application/json')) { return c.json({ error: 'Redirect requests must use application/json' }, 415); } try { const body = await c.req.json(); const { redirect } = (body || {}); if (!redirect || typeof redirect !== 'string') { return c.json({ error: 'Invalid redirect path' }, 400); } const route = await addRedirect(c.get('client'), { path: docPath }, { path: redirect }); return c.json({ success: true, redirect: { id: route.id, path: route.path, document_id: route.document_id, created_at: route.created_at, }, }); } catch (error) { if (error instanceof SyntaxError) { return c.json({ error: 'Invalid JSON body' }, 400); } console.error('Redirect error:', error); return c.json({ error: error instanceof Error ? error.message : 'Redirect creation failed' }, 500); } }; // POST: Preview render export const previewRender = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } try { const body = await c.req.json().catch(() => ({})); const uploadsPath = c.get('uploadsPath'); const result = await getDocumentClientState(c.get('client'), { path: docPath }, body, c.req.query(), { uploadsPath, }); return c.json(result); } catch (error) { console.error('Document client state error:', error); return c.json({ error: 'Failed to process request', details: error instanceof Error ? error.message : 'Unknown error', }, 500); } }; // DELETE: Delete upload export const deleteUpload = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const url = new URL(c.req.url); const uploadFilename = url.searchParams.get('upload'); if (!uploadFilename) { return next(); } try { const uploadsPath = c.get('uploadsPath'); const result = await removeUpload(c.get('client'), { path: docPath }, uploadsPath, uploadFilename); return c.json({ success: true, filename: result.filename, original_filename: result.original_filename, }); } catch (error) { console.error('Upload deletion error:', error); return c.json({ error: error instanceof Error ? error.message : 'Upload deletion failed' }, 500); } }; // PATCH: Update upload (rename or toggle hidden status) export const updateUploadHandler = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const url = new URL(c.req.url); const uploadId = url.searchParams.get('uploadId'); if (!uploadId) { return next(); } const contentType = c.req.header('content-type') || ''; if (!contentType.includes('application/json')) { return c.json({ error: 'Update upload requests must use application/json' }, 415); } try { const body = await c.req.json(); const { original_filename, hidden } = body; const result = await updateUpload(c.get('client'), parseInt(uploadId, 10), { original_filename, hidden, }); return c.json({ success: true, id: result.id, filename: result.filename, original_filename: result.original_filename, hidden: result.hidden, url: `./uploads/${encodeURIComponent(result.original_filename)}`, }); } catch (error) { console.error('Upload update error:', error); return c.json({ error: error instanceof Error ? error.message : 'Upload update failed' }, 500); } }; // DELETE: Delete redirect export const deleteRedirect = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const url = new URL(c.req.url); const redirectPath = url.searchParams.get('redirect'); if (!redirectPath) { return next(); } try { const deleted = await removeRedirect(c.get('client'), redirectPath); if (!deleted) { return c.json({ error: 'Redirect not found or could not be deleted' }, 404); } return c.json({ success: true, path: redirectPath, }); } catch (error) { console.error('Redirect deletion error:', error); return c.json({ error: error instanceof Error ? error.message : 'Redirect deletion failed' }, 500); } }; // DELETE: Delete document export const deleteDocument = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const url = new URL(c.req.url); const shouldDeleteDocument = url.searchParams.get('remove'); if (!shouldDeleteDocument) { return next(); } try { await removeDocument(c.get('client'), { path: docPath }); // Redirect to the document's edit page (will show empty editor since document is deleted) const redirectPath = docPath === '/' ? '/edit' : `${docPath}/edit`; return c.redirect(redirectPath, 302); } catch (error) { console.error('Document deletion error:', error); // Redirect to the document's edit page even on error const redirectPath = docPath === '/' ? '/edit' : `${docPath}/edit`; return c.redirect(redirectPath, 302); } }; // POST: Delete document (for form submissions) export const deleteDocumentPost = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } try { await removeDocument(c.get('client'), { path: docPath }); // Redirect to the document's edit page (will show empty editor since document is deleted) const redirectPath = docPath === '/' ? '/edit' : `${docPath}/edit`; return c.redirect(redirectPath, 302); } catch (error) { console.error('Document deletion error:', error); // Redirect to the document's edit page even on error const redirectPath = docPath === '/' ? '/edit' : `${docPath}/edit`; return c.redirect(redirectPath, 302); } }; // POST: Update document from archive (Content-Type: application/gzip) // This replaces the previous /edit/update endpoint; requests should POST to /edit. export const updateFromArchive = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const contentType = c.req.header('content-type'); const isGzip = Boolean(contentType?.includes('application/gzip') || contentType?.includes('application/x-gzip')); if (!isGzip) { return c.json({ error: 'Update requests must use application/gzip' }, 415); } const contentDisposition = c.req.header('content-disposition'); if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/); if (filenameMatch) { const filename = filenameMatch[1]; if (!filename.endsWith('.tar.gz') && !filename.endsWith('.tgz')) { return c.json({ error: 'Archive must be a .tar.gz or .tgz file' }, 400); } } } try { const webStream = c.req.raw.body; if (!webStream) { return c.json({ error: 'No archive data provided' }, 400); } const nodeStream = Readable.fromWeb(webStream); await updateViaArchive(c.get('client'), { path: docPath }, nodeStream); return c.json({ success: true, message: 'Document updated successfully', path: docPath, }); } catch (error) { console.error('Update error:', error); return c.json({ error: error instanceof Error ? error.message : 'Update failed' }, 400); } }; // POST: Function context API (POST /edit?fn=) // Moved here so /edit is a single consolidated endpoint. export const functionContextHandler = async (c, next) => { const docPath = c.get('docPath'); if (!docPath) { return next(); } const fnName = c.req.query('fn'); if (!fnName) { return c.json({ error: 'fn query parameter required' }, 400); } try { const body = await c.req.json(); // This context is only used as a fallback; getUploads requires an explicit path. const minimalDoc = { path: docPath }; const fn = functionContext(c.get('client'), minimalDoc, c.req.query()); switch (fnName) { case 'getPage': { const { query } = body; if (!query) { return c.json({ error: 'query parameter required' }, 400); } const result = await fn.getPage(query); return c.json(result); } case 'getPages': { const { options } = body; const result = await fn.getPages(options); return c.json(result); } case 'getUploads': { const { options } = body; if (!options?.path) { return c.json({ error: 'path required in options' }, 400); } const result = await fn.getUploads(options); return c.json(result); } default: return c.json({ error: `Unknown function: ${fnName}` }, 400); } } catch (error) { console.error('Function context error:', error); return c.json({ error: 'Function execution failed', details: error instanceof Error ? error.message : 'Unknown error', }, 500); } }; //# sourceMappingURL=edit.js.map