Files
storycove/docs/API.md
2025-07-24 08:51:45 +02:00

1056 lines
21 KiB
Markdown

# 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": "<p>Story content</p>", // 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<AuthorDto>`
#### 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": "<p>HTML content</p>",
"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<T>
```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.