diff --git a/api-endpoints.md b/api-endpoints.md new file mode 100644 index 0000000..6f96119 --- /dev/null +++ b/api-endpoints.md @@ -0,0 +1,422 @@ +# SimpleNote Web - API Endpoints + +**Base URL:** `/api/v1` +**Auth:** All endpoints require `Authorization: Bearer ` header unless noted. + +--- + +## Auth + +### `POST /auth/token` +Generate a new API token (admin only). + +**Request body:** +```json +{ "label": "my-token-label" } +``` + +**Response `201`:** +```json +{ "token": "snk_...", "label": "my-token-label", "createdAt": "2026-03-28T..." } +``` + +--- + +### `GET /auth/verify` +Verify a token is valid. + +**Response `200`:** +```json +{ "valid": true, "token": "snk_..." } +``` + +--- + +## Projects + +### `GET /projects` +List all projects. + +**Response `200`:** +```json +{ "projects": [...] } +``` + +--- + +### `POST /projects` +Create a project. + +**Request body:** +```json +{ "name": "Project Name", "description": "optional" } +``` + +**Response `201`:** +```json +{ "id": "...", "name": "...", "description": "...", "createdAt": "...", "updatedAt": "..." } +``` + +--- + +### `GET /projects/:id` +Get a single project. + +**Response `200`:** +```json +{ "id": "...", "name": "...", "description": "...", "createdAt": "...", "updatedAt": "..." } +``` + +**Response `404`:** +```json +{ "error": "Project not found", "code": "NOT_FOUND" } +``` + +--- + +### `PUT /projects/:id` +Update a project. + +**Request body:** +```json +{ "name": "New Name", "description": "New desc" } +``` + +**Response `200`:** +```json +{ "id": "...", "name": "...", "description": "...", "createdAt": "...", "updatedAt": "..." } +``` + +--- + +### `DELETE /projects/:id` +Delete a project. + +**Response `200`:** +```json +{ "deleted": true, "id": "..." } +``` + +--- + +### `GET /projects/:id/tree` +Get full project tree (folder hierarchy + documents). + +**Response `200`:** +```json +{ + "project": { "id": "...", "name": "..." }, + "documents": [...], + "folders": [...], + "totalDocuments": 5 +} +``` + +--- + +### `GET /projects/:id/documents` +List documents directly in a project (not in sub-folders). + +**Response `200`:** +```json +{ "documents": [...], "total": 3 } +``` + +--- + +## Folders + +### `GET /folders?projectId=X&parentId=Y` +List folders in a project. `projectId` is required. + +**Query params:** +- `projectId` (required): Project ID +- `parentId` (optional): Parent folder ID. Omit for root folders. + +**Response `200`:** +```json +{ "folders": [...] } +``` + +**Response `400`:** +```json +{ "error": "projectId query parameter is required", "code": "VALIDATION_ERROR" } +``` + +--- + +### `POST /folders` +Create a folder. + +**Request body:** +```json +{ "name": "Folder Name", "projectId": "...", "parentId": "..." } +``` + +**Response `201`:** +```json +{ "id": "...", "name": "...", "projectId": "...", "parentId": null, "createdAt": "...", "updatedAt": "..." } +``` + +--- + +### `GET /folders/:id` +Get a single folder. + +**Response `200`:** +```json +{ "id": "...", "name": "...", "projectId": "...", "parentId": null, "createdAt": "...", "updatedAt": "..." } +``` + +--- + +### `PUT /folders/:id` +Update a folder (rename). + +**Request body:** +```json +{ "name": "New Folder Name" } +``` + +**Response `200`:** +```json +{ "id": "...", "name": "New Folder Name", ... } +``` + +--- + +### `DELETE /folders/:id` +Delete a folder. + +**Response `200`:** +```json +{ "deleted": true, "id": "..." } +``` + +--- + +### `GET /folders/:id/documents` +List documents directly in a folder. + +**Response `200`:** +```json +{ "documents": [...], "total": 2 } +``` + +--- + +### `GET /folders/:id/tree` +Get full folder tree (sub-folders + documents recursively). + +**Response `200`:** +```json +{ + "folder": { "id": "...", "name": "..." }, + "documents": [...], + "subFolders": [...], + "totalDocuments": 4 +} +``` + +--- + +## Documents + +### `GET /documents` +List documents with optional filters. + +**Query params:** +- `tag` - Filter by tag +- `library` - Filter by library ID +- `project` - Filter by project ID +- `folder` - Filter by folder ID +- `type` - Filter by type (e.g., `requirement`, `general`) +- `status` - Filter by status (e.g., `draft`, `approved`) +- `limit` - Max results (default 50) +- `offset` - Pagination offset (default 0) + +**Response `200`:** +```json +{ "documents": [...], "total": 10, "limit": 50, "offset": 0 } +``` + +--- + +### `POST /documents` +Create a document. + +**Request body:** +```json +{ + "title": "Doc Title", + "libraryId": "...", + "projectId": "...", + "folderId": "...", + "content": "# Markdown content", + "tags": ["tag1", "tag2"], + "type": "requirement", + "priority": "high", + "status": "draft" +} +``` + +**Response `201`:** +```json +{ "id": "...", "title": "...", "content": "...", "tags": [...], "type": "...", "priority": "...", "status": "...", "createdAt": "...", "updatedAt": "..." } +``` + +--- + +### `GET /documents/:id` +Get a single document. + +**Response `200`:** +```json +{ "id": "...", "title": "...", "content": "...", "tags": [...], "type": "...", "priority": "...", "status": "...", "createdAt": "...", "updatedAt": "..." } +``` + +--- + +### `PUT /documents/:id` +Update a document. + +**Request body:** +```json +{ "title": "New Title", "content": "...", "tags": [...], "type": "...", "priority": "...", "status": "...", "folderId": "..." } +``` + +**Response `200`:** Document object. + +--- + +### `DELETE /documents/:id` +Delete a document. + +**Response `200`:** +```json +{ "deleted": true, "id": "..." } +``` + +--- + +### `GET /documents/:id/export` +Export document as markdown. + +**Response `200`:** Raw text/markdown (Content-Type: `text/markdown`). + +--- + +### `POST /documents/:id/tags` +Add tags to a document. + +**Request body:** +```json +{ "tags": ["new-tag", "another"] } +``` + +**Response `200`:** Updated document object. + +--- + +## Tags + +### `GET /tags` +List all tags with counts. + +**Response `200`:** +```json +{ "tags": [{ "name": "backend", "count": 5 }, ...], "total": 12 } +``` + +--- + +### `GET /tags/:tag` +Get all documents with a specific tag. + +**Response `200`:** +```json +{ "tag": "backend", "documents": [...], "count": 5 } +``` + +--- + +## Libraries + +### `GET /libraries` +List root libraries. + +**Response `200`:** +```json +{ "libraries": [...] } +``` + +--- + +### `POST /libraries` +Create a library. + +**Request body:** +```json +{ "name": "Library Name", "parentId": "..." } +``` + +**Response `201`:** +```json +{ "id": "...", "name": "...", "parentId": null, "createdAt": "...", "updatedAt": "..." } +``` + +--- + +### `GET /libraries/:id` +Get library contents. + +**Response `200`:** +```json +{ "id": "...", "name": "...", "parentId": null, ... } +``` + +--- + +### `GET /libraries/:id/tree` +Get full library tree. + +**Response `200`:** +```json +{ + "library": { ... }, + "documents": [...], + "subLibraries": [...], + "totalDocuments": 3 +} +``` + +--- + +### `GET /libraries/:id/documents` +List documents in a library. + +**Response `200`:** +```json +{ "documents": [...], "total": 3 } +``` + +--- + +### `DELETE /libraries/:id` +Delete a library. + +**Response `200`:** +```json +{ "deleted": true, "id": "..." } +``` + +--- + +## Error Format + +All errors follow this structure: +```json +{ "error": "Human-readable message", "code": "ERROR_CODE" } +``` + +Common codes: `VALIDATION_ERROR`, `NOT_FOUND`, `UNAUTHORIZED`, `INTERNAL_ERROR` diff --git a/public/js/api.js b/public/js/api.js index 1c35e14..75ebe8f 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -36,7 +36,7 @@ class ApiClient { } const response = await fetch(`${API_BASE}${path}`, options); - + if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Request failed' })); throw new Error(error.message || `HTTP ${response.status}`); @@ -50,7 +50,7 @@ class ApiClient { put(path, body) { return this.request('PUT', path, body); } delete(path) { return this.request('DELETE', path); } - // Auth + // ===== Auth ===== async login(token) { try { this.setToken(token); // Set token BEFORE making request @@ -62,7 +62,7 @@ class ApiClient { } } - // Documents + // ===== Documents ===== getDocuments(params = {}) { const query = new URLSearchParams(params).toString(); return this.get(`/documents${query ? '?' + query : ''}`); @@ -90,7 +90,25 @@ class ApiClient { }).then(r => r.text()); } - // Libraries + addDocumentTags(documentId, tags) { + return this.post(`/documents/${documentId}/tags`, { tags }); + } + + moveDocumentToFolder(documentId, folderId) { + return this.put(`/documents/${documentId}`, { folderId }); + } + + // ===== Tags ===== + getTags() { + return this.get('/tags'); + } + + // GET /tags/:tag - get documents by tag + getDocumentsByTag(tag) { + return this.get(`/tags/${encodeURIComponent(tag)}`); + } + + // ===== Libraries ===== getLibraries() { return this.get('/libraries'); } @@ -111,12 +129,17 @@ class ApiClient { return this.delete(`/libraries/${id}`); } - // Tags - getTags() { - return this.get('/tags'); + // GET /libraries/:id/tree + getLibraryTree(id) { + return this.get(`/libraries/${id}/tree`); } - // ===== PROJECTS ===== + // GET /libraries/:id/documents + getLibraryDocuments(id) { + return this.get(`/libraries/${id}/documents`); + } + + // ===== Projects ===== getProjects() { return this.get('/projects'); } @@ -137,17 +160,27 @@ class ApiClient { return this.delete(`/projects/${id}`); } + // GET /projects/:id/tree getProjectTree(id) { return this.get(`/projects/${id}/tree`); } - // ===== FOLDERS ===== + // GET /projects/:id/documents + getProjectDocuments(id) { + return this.get(`/projects/${id}/documents`); + } + + // ===== Folders ===== getFolders(projectId, parentId = null) { const params = new URLSearchParams({ projectId }); if (parentId) params.append('parentId', parentId); return this.get(`/folders?${params.toString()}`); } + getFolder(id) { + return this.get(`/folders/${id}`); + } + createFolder(data) { return this.post('/folders', data); } @@ -160,9 +193,14 @@ class ApiClient { return this.delete(`/folders/${id}`); } - // ===== Move document to folder ===== - moveDocumentToFolder(documentId, folderId) { - return this.put(`/documents/${documentId}`, { folderId }); + // GET /folders/:id/documents + getFolderDocuments(id) { + return this.get(`/folders/${id}/documents`); + } + + // GET /folders/:id/tree + getFolderTree(id) { + return this.get(`/folders/${id}/tree`); } }