# 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.