# StoryCove REST API Documentation ## Overview StoryCove provides a comprehensive REST API for managing stories, authors, tags, and series. All endpoints use JSON for request and response data, follow RESTful conventions, and require JWT authentication (except for login). **Base URL**: `/api` **Authentication**: JWT tokens via httpOnly cookies **Content-Type**: `application/json` (unless otherwise specified) ## Authentication All API endpoints (except authentication endpoints) require JWT authentication. Tokens are provided via httpOnly cookies and expire after 24 hours. ### Authentication Flow 1. **Login**: `POST /api/auth/login` with password 2. **Receive Token**: Server sets httpOnly cookie with JWT 3. **Access Protected Endpoints**: Cookie automatically sent with requests 4. **Token Expiry**: 24 hours, requires re-authentication --- ## Authentication Endpoints ### POST /api/auth/login **Description**: Authenticate with application password and receive JWT token. **Request Body**: ```json { "password": "string" // Required, application password } ``` **Response (200 OK)**: ```json { "message": "Authentication successful", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` **Response (401 Unauthorized)**: ```json { "error": "Invalid password" } ``` **Side Effects**: Sets httpOnly cookie `token` with 24-hour expiration. ### POST /api/auth/logout **Description**: Clear authentication token and logout. **Request Body**: None **Response (200 OK)**: ```json { "message": "Logged out successfully" } ``` **Side Effects**: Clears the `token` cookie. ### GET /api/auth/verify **Description**: Verify if current JWT token is valid. **Authentication**: Required **Response (200 OK)**: ```json { "message": "Token is valid" } ``` **Response (401 Unauthorized)**: ```json { "error": "Token is invalid or expired" } ``` --- ## Story Management Endpoints ### GET /api/stories **Description**: Get paginated list of all stories. **Query Parameters**: - `page` (integer, default: 0): Page number (0-based) - `size` (integer, default: 20): Page size (max: 100) - `sortBy` (string, default: "createdAt"): Sort field - `sortDir` (string, default: "desc"): Sort direction (asc/desc) **Response (200 OK)**: ```json { "content": [StoryDto], "pageable": { "sort": { "sorted": true, "unsorted": false }, "pageNumber": 0, "pageSize": 20, "offset": 0, "paged": true, "unpaged": false }, "totalElements": 150, "totalPages": 8, "last": false, "first": true, "numberOfElements": 20, "size": 20, "number": 0, "sort": { "sorted": true, "unsorted": false }, "empty": false } ``` ### GET /api/stories/{id} **Description**: Get specific story by UUID. **Path Parameters**: - `id` (UUID): Story identifier **Response (200 OK)**: `StoryDto` **Response (404 Not Found)**: ```json { "error": "Story not found with id: {id}" } ``` ### POST /api/stories **Description**: Create a new story. **Request Body**: ```json { "title": "string", // Required "summary": "string", // Optional "description": "string", // Optional "contentHtml": "

Story content

", // Optional "sourceUrl": "https://example.com", // Optional "volume": 1, // Optional, integer "authorId": "uuid", // Optional, use either authorId or authorName "authorName": "Author Name", // Optional, creates author if doesn't exist "seriesId": "uuid", // Optional, use either seriesId or seriesName "seriesName": "Series Name", // Optional, creates series if doesn't exist "tagNames": ["tag1", "tag2"] // Optional, creates tags if they don't exist } ``` **Response (201 Created)**: `StoryDto` **Response (400 Bad Request)**: ```json { "error": "Title is required", "details": { "title": "must not be blank" } } ``` ### PUT /api/stories/{id} **Description**: Update existing story. All fields are optional. **Path Parameters**: - `id` (UUID): Story identifier **Request Body**: Same as POST, all fields optional **Response (200 OK)**: `StoryDto` ### DELETE /api/stories/{id} **Description**: Delete story and all associated relationships. **Path Parameters**: - `id` (UUID): Story identifier **Response (200 OK)**: ```json { "message": "Story deleted successfully" } ``` ### POST /api/stories/{id}/cover **Description**: Upload cover image for story. **Path Parameters**: - `id` (UUID): Story identifier **Content-Type**: `multipart/form-data` **Form Parameters**: - `file` (File): Image file (JPG, PNG, WebP, max 5MB) **Response (200 OK)**: ```json { "message": "Cover uploaded successfully", "coverPath": "covers/story-uuid-cover.jpg", "coverUrl": "/api/files/images/covers/story-uuid-cover.jpg" } ``` ### DELETE /api/stories/{id}/cover **Description**: Remove cover image from story. **Path Parameters**: - `id` (UUID): Story identifier **Response (200 OK)**: ```json { "message": "Cover removed successfully" } ``` ### POST /api/stories/{id}/rating **Description**: Set story rating. **Path Parameters**: - `id` (UUID): Story identifier **Request Body**: ```json { "rating": 4 // Integer, 1-5 } ``` **Response (200 OK)**: `StoryDto` ### Tag Management for Stories #### POST /api/stories/{id}/tags/{tagId} **Description**: Add existing tag to story. **Path Parameters**: - `id` (UUID): Story identifier - `tagId` (UUID): Tag identifier **Response (200 OK)**: `StoryDto` #### DELETE /api/stories/{id}/tags/{tagId} **Description**: Remove tag from story. **Path Parameters**: - `id` (UUID): Story identifier - `tagId` (UUID): Tag identifier **Response (200 OK)**: `StoryDto` ### Story Search and Filtering #### GET /api/stories/search **Description**: Search stories using Typesense full-text search. **Query Parameters**: - `query` (string, required): Search query - `page` (integer, default: 0): Page number - `size` (integer, default: 20): Page size - `authors` (string[]): Filter by author names - `tags` (string[]): Filter by tag names - `minRating` (integer): Minimum rating filter - `maxRating` (integer): Maximum rating filter - `sortBy` (string): Sort field - `sortDir` (string): Sort direction **Response (200 OK)**: ```json { "results": [StorySearchDto], "totalHits": 42, "page": 0, "perPage": 20, "query": "search terms", "searchTimeMs": 15 } ``` #### GET /api/stories/search/suggestions **Description**: Get search query suggestions. **Query Parameters**: - `query` (string, required): Partial search query - `limit` (integer, default: 5): Number of suggestions **Response (200 OK)**: ```json ["suggestion 1", "suggestion 2", "suggestion 3"] ``` ### Story Collections #### GET /api/stories/author/{authorId} **Description**: Get stories by specific author. **Path Parameters**: - `authorId` (UUID): Author identifier **Query Parameters**: Standard pagination parameters **Response (200 OK)**: Paginated `StoryDto` list #### GET /api/stories/series/{seriesId} **Description**: Get stories in series, ordered by volume. **Path Parameters**: - `seriesId` (UUID): Series identifier **Response (200 OK)**: `StoryDto[]` (ordered by volume) #### GET /api/stories/tags/{tagName} **Description**: Get stories with specific tag. **Path Parameters**: - `tagName` (string): Tag name **Query Parameters**: Standard pagination parameters **Response (200 OK)**: Paginated `StoryDto` list #### GET /api/stories/recent **Description**: Get recently added stories. **Query Parameters**: - `limit` (integer, default: 10): Number of stories **Response (200 OK)**: `StoryDto[]` #### GET /api/stories/top-rated **Description**: Get top-rated stories. **Query Parameters**: - `limit` (integer, default: 10): Number of stories **Response (200 OK)**: `StoryDto[]` --- ## Author Management Endpoints ### GET /api/authors **Description**: Get paginated list of all authors. **Query Parameters**: Standard pagination and sorting parameters **Response (200 OK)**: Paginated `AuthorDto` list ### GET /api/authors/{id} **Description**: Get specific author by UUID. **Path Parameters**: - `id` (UUID): Author identifier **Response (200 OK)**: `AuthorDto` ### POST /api/authors **Description**: Create new author. **Request Body**: ```json { "name": "Author Name", // Required, unique "notes": "Notes about the author", // Optional "urls": ["https://example.com"] // Optional, list of URLs } ``` **Response (201 Created)**: `AuthorDto` ### PUT /api/authors/{id} (JSON) **Description**: Update author with JSON data. **Path Parameters**: - `id` (UUID): Author identifier **Content-Type**: `application/json` **Request Body**: ```json { "name": "Updated Name", // Optional "notes": "Updated notes", // Optional "urls": ["url1", "url2"], // Optional, replaces all URLs "rating": 4 // Optional, 1-5 } ``` **Response (200 OK)**: `AuthorDto` ### PUT /api/authors/{id} (Multipart) **Description**: Update author with multipart form data (supports avatar upload). **Path Parameters**: - `id` (UUID): Author identifier **Content-Type**: `multipart/form-data` **Form Parameters**: - `name` (string): Author name - `notes` (string): Author notes - `urls` (string[]): List of URLs - `authorRating` (integer): Author rating - `avatar` (File): Avatar image file **Response (200 OK)**: `AuthorDto` ### DELETE /api/authors/{id} **Description**: Delete author and all associated data. **Path Parameters**: - `id` (UUID): Author identifier **Response (200 OK)**: ```json { "message": "Author deleted successfully" } ``` ### Author Image Management #### POST /api/authors/{id}/avatar **Description**: Upload avatar image for author. **Path Parameters**: - `id` (UUID): Author identifier **Content-Type**: `multipart/form-data` **Form Parameters**: - `file` (File): Avatar image (JPG, PNG, WebP, max 5MB) **Response (200 OK)**: ```json { "message": "Avatar uploaded successfully", "avatarPath": "avatars/author-uuid-avatar.jpg", "avatarUrl": "/api/files/images/avatars/author-uuid-avatar.jpg" } ``` #### DELETE /api/authors/{id}/avatar **Description**: Remove avatar image from author. **Response (200 OK)**: ```json { "message": "Avatar removed successfully" } ``` ### Author URL Management #### POST /api/authors/{id}/urls **Description**: Add URL to author. **Path Parameters**: - `id` (UUID): Author identifier **Request Body**: ```json { "url": "https://example.com" } ``` **Response (200 OK)**: `AuthorDto` #### DELETE /api/authors/{id}/urls **Description**: Remove URL from author. **Request Body**: ```json { "url": "https://example.com" } ``` **Response (200 OK)**: `AuthorDto` ### Author Search and Collections #### GET /api/authors/search **Description**: Search authors by name (database search). **Query Parameters**: - `query` (string, required): Search query - Standard pagination parameters **Response (200 OK)**: Paginated `AuthorDto` list #### GET /api/authors/search-typesense **Description**: Search authors using Typesense. **Query Parameters**: - `q` (string, default: "*"): Search query - `page`, `size`: Pagination - `sortBy` (string, default: "name"): Sort field - `sortOrder` (string, default: "asc"): Sort order **Response (200 OK)**: `SearchResultDto` #### GET /api/authors/top-rated **Description**: Get top-rated authors. **Query Parameters**: - `limit` (integer, default: 10): Number of authors **Response (200 OK)**: `AuthorDto[]` --- ## Tag Management Endpoints ### GET /api/tags **Description**: Get paginated list of all tags. **Query Parameters**: Standard pagination and sorting parameters **Response (200 OK)**: Paginated `TagDto` list ### GET /api/tags/{id} **Description**: Get specific tag by UUID. **Response (200 OK)**: `TagDto` ### POST /api/tags **Description**: Create new tag. **Request Body**: ```json { "name": "tag-name" // Required, max 100 characters, will be lowercased } ``` **Response (201 Created)**: `TagDto` ### PUT /api/tags/{id} **Description**: Update tag name. **Request Body**: ```json { "name": "new-tag-name" // Optional } ``` **Response (200 OK)**: `TagDto` ### DELETE /api/tags/{id} **Description**: Delete tag and remove from all stories. **Response (200 OK)**: ```json { "message": "Tag deleted successfully" } ``` ### Tag Collections and Search #### GET /api/tags/search **Description**: Search tags by name. **Query Parameters**: - `query` (string, required): Search query - Standard pagination parameters **Response (200 OK)**: Paginated `TagDto` list #### GET /api/tags/autocomplete **Description**: Get tag suggestions for autocomplete. **Query Parameters**: - `query` (string, required): Partial tag name - `limit` (integer, default: 10): Number of suggestions **Response (200 OK)**: `TagDto[]` #### GET /api/tags/popular **Description**: Get most used tags. **Query Parameters**: - `limit` (integer, default: 20): Number of tags **Response (200 OK)**: `TagDto[]` (ordered by usage count) #### GET /api/tags/unused **Description**: Get tags not used by any stories. **Response (200 OK)**: `TagDto[]` #### GET /api/tags/stats **Description**: Get tag usage statistics. **Response (200 OK)**: ```json { "totalTags": 150, "usedTags": 120, "unusedTags": 30 } ``` --- ## Series Management Endpoints ### GET /api/series **Description**: Get paginated list of all series. **Response (200 OK)**: Paginated `SeriesDto` list ### GET /api/series/{id} **Description**: Get specific series by UUID. **Response (200 OK)**: `SeriesDto` ### POST /api/series **Description**: Create new series. **Request Body**: ```json { "name": "Series Name", // Required, max 255 characters "description": "Description" // Optional, max 1000 characters } ``` **Response (201 Created)**: `SeriesDto` ### PUT /api/series/{id} **Description**: Update series. **Request Body**: ```json { "name": "Updated Name", // Optional "description": "Updated desc" // Optional } ``` **Response (200 OK)**: `SeriesDto` ### DELETE /api/series/{id} **Description**: Delete series and remove series association from stories. **Response (200 OK)**: ```json { "message": "Series deleted successfully" } ``` ### Series Collections #### GET /api/series/search **Description**: Search series by name. **Query Parameters**: - `query` (string, required): Search query - Standard pagination parameters **Response (200 OK)**: Paginated `SeriesDto` list #### GET /api/series/with-stories **Description**: Get series that have stories. **Query Parameters**: - `limit` (integer, default: 20): Number of series **Response (200 OK)**: `SeriesDto[]` #### GET /api/series/popular **Description**: Get most popular series (by story count). **Query Parameters**: - `limit` (integer, default: 10): Number of series **Response (200 OK)**: `SeriesDto[]` #### GET /api/series/empty **Description**: Get series with no stories. **Response (200 OK)**: `SeriesDto[]` #### GET /api/series/stats **Description**: Get series statistics. **Response (200 OK)**: ```json { "totalSeries": 50, "seriesWithStories": 35, "emptySeries": 15 } ``` --- ## File Management Endpoints ### POST /api/files/upload/cover **Description**: Upload cover image (generic endpoint). **Content-Type**: `multipart/form-data` **Form Parameters**: - `file` (File): Image file **Response (200 OK)**: ```json { "message": "Cover uploaded successfully", "path": "covers/filename.jpg", "url": "/api/files/images/covers/filename.jpg" } ``` ### POST /api/files/upload/avatar **Description**: Upload avatar image (generic endpoint). **Response (200 OK)**: ```json { "message": "Avatar uploaded successfully", "path": "avatars/filename.jpg", "url": "/api/files/images/avatars/filename.jpg" } ``` ### GET /api/files/images/** **Description**: Serve image files. **Query Parameters**: - `path` (string, required): Image path **Response**: Image file with appropriate Content-Type **Headers**: - `Cache-Control: max-age=31536000` (1 year) - `Content-Type: image/jpeg|png|webp` ### DELETE /api/files/images **Description**: Delete image file. **Query Parameters**: - `path` (string, required): Image path to delete **Response (200 OK)**: ```json { "message": "Image deleted successfully" } ``` --- ## Search and Indexing Endpoints ### POST /api/search/reindex **Description**: Manually reindex all stories in Typesense. **Response (200 OK)**: ```json { "message": "Successfully reindexed all stories", "storiesCount": 1250 } ``` ### GET /api/search/health **Description**: Check search service health status. **Response (200 OK)**: ```json { "status": "healthy", "message": "Typesense is running and responsive" } ``` **Response (503 Service Unavailable)**: ```json { "status": "unhealthy", "message": "Typesense is not responding", "error": "Connection refused" } ``` --- ## Configuration Endpoints ### GET /api/config/html-sanitization **Description**: Get HTML sanitization configuration for frontend. **Response (200 OK)**: ```json { "allowedTags": [ "p", "br", "div", "span", "h1", "h2", "h3", "b", "strong", "i", "em", "u", "s", "del" ], "allowedAttributes": { "p": ["class", "style"], "div": ["class", "style"], "span": ["class", "style"] }, "allowedCssProperties": [ "color", "background-color", "font-size", "font-weight", "font-style", "text-align" ], "removedAttributes": { "a": ["href", "target"] }, "description": "HTML sanitization configuration for StoryCove" } ``` --- ## Data Transfer Objects (DTOs) ### StoryDto ```json { "id": "123e4567-e89b-12d3-a456-426614174000", "title": "Story Title", "summary": "Story summary", "description": "Story description", "contentHtml": "

HTML content

", "contentPlain": "Plain text content", "sourceUrl": "https://example.com/story", "coverPath": "covers/story-cover.jpg", "wordCount": 1500, "rating": 4, "volume": 1, "authorId": "author-uuid", "authorName": "Author Name", "seriesId": "series-uuid", "seriesName": "Series Name", "tags": [ { "id": "tag-uuid", "name": "tag-name", "storyCount": 10, "createdAt": "2024-01-15T10:30:00", "updatedAt": "2024-01-15T10:30:00" } ], "createdAt": "2024-01-15T10:30:00", "updatedAt": "2024-01-16T14:20:00" } ``` ### AuthorDto ```json { "id": "author-uuid", "name": "Author Name", "notes": "Notes about the author", "avatarImagePath": "avatars/author-avatar.jpg", "authorRating": 4, "averageStoryRating": 3.8, "urls": [ "https://authorwebsite.com", "https://twitter.com/author" ], "storyCount": 15, "createdAt": "2024-01-10T09:00:00", "updatedAt": "2024-01-16T11:00:00" } ``` ### TagDto ```json { "id": "tag-uuid", "name": "fantasy", "storyCount": 25, "createdAt": "2024-01-05T12:00:00", "updatedAt": "2024-01-05T12:00:00" } ``` ### SeriesDto ```json { "id": "series-uuid", "name": "Series Name", "description": "Series description", "storyCount": 5, "createdAt": "2024-01-01T00:00:00" } ``` ### SearchResultDto ```json { "results": [T], // Array of result objects "totalHits": 42, "page": 0, "perPage": 20, "query": "search terms", "searchTimeMs": 15 } ``` --- ## Error Handling ### Standard HTTP Status Codes - **200 OK**: Request successful - **201 Created**: Resource created successfully - **400 Bad Request**: Invalid request data or validation error - **401 Unauthorized**: Authentication required or invalid token - **403 Forbidden**: Access denied - **404 Not Found**: Resource not found - **409 Conflict**: Resource conflict (e.g., duplicate name) - **413 Payload Too Large**: File upload too large - **415 Unsupported Media Type**: Invalid file type - **422 Unprocessable Entity**: Validation error - **500 Internal Server Error**: Server error ### Error Response Format **Single Error**: ```json { "error": "Error message" } ``` **Validation Errors**: ```json { "error": "Validation failed", "details": { "title": "must not be blank", "rating": "must be between 1 and 5" } } ``` **Field-Specific Error**: ```json { "error": "Author not found with id: 123e4567-e89b-12d3-a456-426614174000" } ``` --- ## Rate Limiting and Performance ### Request Limits - No explicit rate limiting implemented - File uploads limited to 5MB per file - Page size limited to 100 items maximum - Search queries have reasonable timeout limits ### Performance Considerations - Pagination used for all list endpoints - Lazy loading for entity relationships - Image files cached with 1-year expiration - Search powered by Typesense for fast full-text search - Database queries optimized with proper indexing ### Best Practices - Use pagination for large datasets - Cache images and static content - Use search endpoints for finding content rather than filtering - Batch operations when possible - Include only necessary fields in responses This API documentation provides comprehensive coverage of all StoryCove endpoints with examples, error handling, and best practices for integration.