diff --git a/README.md b/README.md index 4c4729a..3821032 100644 --- a/README.md +++ b/README.md @@ -103,14 +103,259 @@ cd backend - `docker-compose logs -f [service]` - View logs - `docker-compose build` - Rebuild containers -## Features +## ✨ Features -- Story management with HTML content support -- Author profiles with ratings and metadata -- Tag-based categorization -- Full-text search capabilities -- Responsive reading interface -- JWT-based authentication -- Docker-based deployment +### 📚 **Story Management** +- **Rich Text Support**: HTML content with automatic plain text extraction +- **Content Sanitization**: Secure HTML processing with customizable sanitization rules +- **Metadata Management**: Title, summary, description, source URL tracking +- **Rating System**: 5-star rating system for stories +- **Word Count**: Automatic word count calculation +- **Cover Images**: Upload and manage story cover images +- **Series Support**: Organize stories into series with volume numbers -For detailed specifications, see `storycove-spec.md`. \ No newline at end of file +### 👤 **Author Management** +- **Author Profiles**: Comprehensive author information with notes +- **Avatar Images**: Upload and manage author profile pictures +- **URL Collections**: Track multiple URLs per author (websites, social media, etc.) +- **Author Ratings**: Rate authors with 5-star system +- **Statistics**: View author statistics and story counts + +### 🏷️ **Organization & Discovery** +- **Tag System**: Flexible tagging with autocomplete +- **Series Organization**: Group related stories with volume ordering +- **Search & Filter**: Full-text search powered by Typesense +- **Advanced Filtering**: Filter by author, tags, ratings, and more +- **Smart Suggestions**: Auto-complete for tags and search queries + +### 🎨 **User Experience** +- **Dark/Light Mode**: Automatic theme switching with system preference detection +- **Responsive Design**: Optimized for desktop, tablet, and mobile +- **Reading Mode**: Distraction-free reading interface +- **Keyboard Navigation**: Full keyboard accessibility +- **Rich Text Editor**: Visual and source editing modes for story content + +### 🔒 **Security & Administration** +- **JWT Authentication**: Secure token-based authentication +- **Single Password**: Simplified access control +- **HTML Sanitization**: Prevent XSS attacks with configurable sanitization +- **CORS Configuration**: Environment-specific CORS settings +- **File Upload Security**: Secure image upload with validation + +### 🚀 **Technical Features** +- **Full-Text Search**: Powered by Typesense search engine +- **RESTful API**: Comprehensive REST API for all operations +- **Database Optimization**: PostgreSQL with proper indexing +- **Image Processing**: Automatic image optimization and storage +- **Environment Configuration**: Multi-environment deployment support +- **Docker Deployment**: Complete containerized deployment + +### 📊 **Analytics & Statistics** +- **Usage Statistics**: Track story counts, tag usage, and more +- **Rating Analytics**: View average ratings and distributions +- **Search Analytics**: Monitor search performance and suggestions +- **Storage Metrics**: Track image storage and database usage + +## 📖 Documentation + +- **[API Documentation](docs/API.md)**: Complete REST API reference with examples +- **[Data Model](docs/DATA_MODEL.md)**: Detailed database schema and relationships +- **[Technical Specification](storycove-spec.md)**: Comprehensive technical specification +- **Environment Configuration**: Multi-environment deployment setup (see above) +- **Development Setup**: Local development environment setup (see below) + +## 🗄️ Data Model + +StoryCove uses a PostgreSQL database with the following core entities: + +### **Stories** +- **Primary Key**: UUID +- **Fields**: title, summary, description, content_html, content_plain, source_url, word_count, rating, volume, cover_path +- **Relationships**: Many-to-One with Author, Many-to-One with Series, Many-to-Many with Tags +- **Features**: Automatic word count calculation, HTML sanitization, plain text extraction + +### **Authors** +- **Primary Key**: UUID +- **Fields**: name, notes, author_rating, avatar_image_path +- **Relationships**: One-to-Many with Stories, One-to-Many with Author URLs +- **Features**: URL collection storage, rating system, statistics calculation + +### **Series** +- **Primary Key**: UUID +- **Fields**: name, description +- **Relationships**: One-to-Many with Stories (ordered by volume) +- **Features**: Volume-based story ordering, navigation methods + +### **Tags** +- **Primary Key**: UUID +- **Fields**: name (unique) +- **Relationships**: Many-to-Many with Stories +- **Features**: Autocomplete support, usage statistics + +### **Join Tables** +- **story_tags**: Links stories to tags +- **author_urls**: Stores multiple URLs per author + +## 🔌 REST API Reference + +### **Authentication** (`/api/auth`) +- `POST /login` - Authenticate with password +- `POST /logout` - Clear authentication token +- `GET /verify` - Verify token validity + +### **Stories** (`/api/stories`) +- `GET /` - List stories (paginated) +- `GET /{id}` - Get specific story +- `POST /` - Create new story +- `PUT /{id}` - Update story +- `DELETE /{id}` - Delete story +- `POST /{id}/cover` - Upload cover image +- `DELETE /{id}/cover` - Remove cover image +- `POST /{id}/rating` - Set story rating +- `POST /{id}/tags/{tagId}` - Add tag to story +- `DELETE /{id}/tags/{tagId}` - Remove tag from story +- `GET /search` - Search stories (Typesense) +- `GET /search/suggestions` - Get search suggestions +- `GET /author/{authorId}` - Stories by author +- `GET /series/{seriesId}` - Stories in series +- `GET /tags/{tagName}` - Stories with tag +- `GET /recent` - Recent stories +- `GET /top-rated` - Top-rated stories + +### **Authors** (`/api/authors`) +- `GET /` - List authors (paginated) +- `GET /{id}` - Get specific author +- `POST /` - Create new author +- `PUT /{id}` - Update author (JSON or multipart) +- `DELETE /{id}` - Delete author +- `POST /{id}/avatar` - Upload avatar image +- `DELETE /{id}/avatar` - Remove avatar image +- `POST /{id}/rating` - Set author rating +- `POST /{id}/urls` - Add URL to author +- `DELETE /{id}/urls` - Remove URL from author +- `GET /search` - Search authors +- `GET /search-typesense` - Advanced author search +- `GET /top-rated` - Top-rated authors + +### **Tags** (`/api/tags`) +- `GET /` - List tags (paginated) +- `GET /{id}` - Get specific tag +- `POST /` - Create new tag +- `PUT /{id}` - Update tag +- `DELETE /{id}` - Delete tag +- `GET /search` - Search tags +- `GET /autocomplete` - Tag autocomplete +- `GET /popular` - Most used tags +- `GET /unused` - Unused tags +- `GET /stats` - Tag statistics + +### **Series** (`/api/series`) +- `GET /` - List series (paginated) +- `GET /{id}` - Get specific series +- `POST /` - Create new series +- `PUT /{id}` - Update series +- `DELETE /{id}` - Delete series +- `GET /search` - Search series +- `GET /with-stories` - Series with stories +- `GET /popular` - Popular series +- `GET /empty` - Empty series +- `GET /stats` - Series statistics + +### **Files** (`/api/files`) +- `POST /upload/cover` - Upload cover image +- `POST /upload/avatar` - Upload avatar image +- `GET /images/**` - Serve image files +- `DELETE /images` - Delete image file + +### **Search** (`/api/search`) +- `POST /reindex` - Reindex all content +- `GET /health` - Search service health + +### **Configuration** (`/api/config`) +- `GET /html-sanitization` - Get HTML sanitization rules + +### **Request/Response Format** +All API endpoints use JSON format with proper HTTP status codes: +- **200**: Success +- **201**: Created +- **400**: Bad Request (validation errors) +- **401**: Unauthorized +- **404**: Not Found +- **500**: Internal Server Error + +### **Authentication** +- JWT tokens provided via httpOnly cookies +- All endpoints (except `/api/auth/login`) require authentication +- Tokens expire after 24 hours + +## 🔧 Development + +### **Technology Stack** +- **Frontend**: Next.js 14, TypeScript, Tailwind CSS, React +- **Backend**: Spring Boot 3, Java 21, PostgreSQL, Typesense +- **Infrastructure**: Docker, Docker Compose, Nginx +- **Security**: JWT authentication, HTML sanitization, CORS + +### **Local Development Setup** + +1. **Prerequisites**: + ```bash + # Required + - Docker & Docker Compose + - Node.js 18+ (for frontend development) + - Java 21+ (for backend development) + ``` + +2. **Environment Setup**: + ```bash + # Copy environment template + cp .env.example .env + + # Edit environment variables + vim .env + ``` + +3. **Database Setup**: + ```bash + # Start database only + docker-compose up -d postgres + ``` + +4. **Backend Development**: + ```bash + cd backend + ./mvnw spring-boot:run + ``` + +5. **Frontend Development**: + ```bash + cd frontend + npm install + npm run dev + ``` + +6. **Full Stack Development**: + ```bash + # Start all services + docker-compose up -d + + # View logs + docker-compose logs -f + ``` + +### **Testing** +```bash +# Backend tests +cd backend && ./mvnw test + +# Frontend build +cd frontend && npm run build + +# Frontend linting +cd frontend && npm run lint +``` + +### **Database Migrations** +The application uses Hibernate with `ddl-auto: update` for schema management. For production deployments, consider using Flyway or Liquibase for controlled migrations. + +For detailed technical specifications, see `storycove-spec.md`. \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..8623322 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,1056 @@ +# 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. \ No newline at end of file diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md new file mode 100644 index 0000000..3a2c407 --- /dev/null +++ b/docs/DATA_MODEL.md @@ -0,0 +1,263 @@ +# StoryCove Data Model Documentation + +## Overview + +StoryCove uses PostgreSQL as its primary database with UUID-based primary keys throughout. The data model is designed to support a personal library of short stories with rich metadata, author information, and flexible organization through tags and series. + +## Entity Relationship Diagram + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ Authors │────│ Stories │────│ Series │ +│ │ │ │ │ │ +│ - id (PK) │ │ - id (PK) │ │ - id (PK) │ +│ - name │ │ - title │ │ - name │ +│ - notes │ │ - content* │ │ - desc │ +│ - rating │ │ - rating │ │ │ +│ - avatar │ │ - volume │ │ │ +└─────────────┘ │ - cover │ └─────────────┘ + │ │ - word_count │ + │ │ - source_url │ + │ │ - timestamps │ + │ └──────────────┘ + │ │ + │ │ +┌─────────────┐ │ ┌─────────────┐ +│ Author_URLs │ │ │ Tags │ +│ │ │ │ │ +│ - author_id │ │ │ - id (PK) │ +│ - url │ │ │ - name │ +└─────────────┘ │ └─────────────┘ + │ │ + │ │ + ┌─────────────┐ │ + │ Story_Tags │─────────┘ + │ │ + │ - story_id │ + │ - tag_id │ + └─────────────┘ +``` + +## Detailed Entity Specifications + +### Stories Table + +**Table Name**: `stories` + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| id | UUID | PRIMARY KEY, NOT NULL | Unique identifier | +| title | VARCHAR(255) | NOT NULL | Story title | +| summary | TEXT | NULL | Optional story summary | +| description | VARCHAR(1000) | NULL | Optional description | +| content_html | TEXT | NULL | HTML content of the story | +| content_plain | TEXT | NULL | Plain text version (auto-generated) | +| source_url | VARCHAR(255) | NULL | Source URL where story was found | +| cover_path | VARCHAR(255) | NULL | Path to cover image file | +| word_count | INTEGER | NOT NULL, DEFAULT 0 | Word count (auto-calculated) | +| rating | INTEGER | NULL, CHECK (rating >= 1 AND rating <= 5) | Story rating | +| volume | INTEGER | NULL | Volume number if part of series | +| author_id | UUID | FOREIGN KEY | Reference to authors table | +| series_id | UUID | FOREIGN KEY, NULL | Reference to series table | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | Creation timestamp | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | Last update timestamp | + +**Indexes:** +- Primary key on `id` +- Foreign key index on `author_id` +- Foreign key index on `series_id` +- Index on `created_at` for recent stories queries +- Index on `rating` for top-rated queries +- Unique constraint on `source_url` where not null + +**Business Rules:** +- Word count is automatically calculated from `content_plain` or `content_html` +- Plain text content is automatically extracted from HTML content using Jsoup +- Volume is only meaningful when series_id is set +- Rating must be between 1-5 if provided + +### Authors Table + +**Table Name**: `authors` + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| id | UUID | PRIMARY KEY, NOT NULL | Unique identifier | +| name | VARCHAR(255) | NOT NULL, UNIQUE | Author name | +| notes | TEXT | NULL | Notes about the author | +| author_rating | INTEGER | NULL, CHECK (author_rating >= 1 AND author_rating <= 5) | Author rating | +| avatar_image_path | VARCHAR(255) | NULL | Path to avatar image | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | Creation timestamp | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | Last update timestamp | + +**Indexes:** +- Primary key on `id` +- Unique index on `name` +- Index on `author_rating` for top-rated queries + +**Business Rules:** +- Author names must be unique across the system +- Rating must be between 1-5 if provided +- Author statistics (story count, average rating) are calculated dynamically + +### Author URLs Table + +**Table Name**: `author_urls` + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| author_id | UUID | FOREIGN KEY, NOT NULL | Reference to authors table | +| url | VARCHAR(255) | NOT NULL | URL associated with author | + +**Indexes:** +- Foreign key index on `author_id` +- Composite index on `(author_id, url)` for uniqueness + +**Business Rules:** +- One author can have multiple URLs +- URLs are stored as simple strings without validation +- Duplicate URLs for the same author are prevented by application logic + +### Series Table + +**Table Name**: `series` + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| id | UUID | PRIMARY KEY, NOT NULL | Unique identifier | +| name | VARCHAR(255) | NOT NULL, UNIQUE | Series name | +| description | VARCHAR(1000) | NULL | Series description | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | Creation timestamp | + +**Indexes:** +- Primary key on `id` +- Unique index on `name` + +**Business Rules:** +- Series names must be unique +- Stories in a series are ordered by volume number +- Series without stories are allowed (placeholder series) + +### Tags Table + +**Table Name**: `tags` + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| id | UUID | PRIMARY KEY, NOT NULL | Unique identifier | +| name | VARCHAR(100) | NOT NULL, UNIQUE | Tag name | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | Creation timestamp | + +**Indexes:** +- Primary key on `id` +- Unique index on `name` +- Index on `name` for autocomplete queries + +**Business Rules:** +- Tag names must be unique and are stored in lowercase +- Tags are created automatically when referenced by stories +- Tag usage statistics are calculated dynamically + +### Story Tags Junction Table + +**Table Name**: `story_tags` + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| story_id | UUID | FOREIGN KEY, NOT NULL | Reference to stories table | +| tag_id | UUID | FOREIGN KEY, NOT NULL | Reference to tags table | + +**Constraints:** +- Primary key on `(story_id, tag_id)` +- Foreign key to `stories(id)` with CASCADE DELETE +- Foreign key to `tags(id)` with CASCADE DELETE + +**Indexes:** +- Composite primary key index +- Index on `tag_id` for reverse lookups + +## Data Types and Conventions + +### UUID Strategy +- All primary keys use UUID (Universally Unique Identifier) +- Generated using `GenerationType.UUID` in Hibernate +- Provides natural uniqueness across distributed systems +- 36-character string representation (e.g., `123e4567-e89b-12d3-a456-426614174000`) + +### Timestamp Management +- All entities have `created_at` timestamp +- Stories and Authors have `updated_at` timestamp (automatically updated) +- Series and Tags only have `created_at` (they're rarely modified) +- All timestamps use `LocalDateTime` in Java, stored as `TIMESTAMP` in PostgreSQL + +### Text Fields +- **VARCHAR(n)**: For constrained text fields (names, paths, URLs) +- **TEXT**: For unlimited text content (story content, notes, descriptions) +- **HTML Content**: Stored as-is but sanitized on input and output +- **Plain Text**: Automatically extracted from HTML using Jsoup + +### Validation Rules +- **Required Fields**: Entity names/titles are always required +- **Length Limits**: Names limited to 255 characters, descriptions to 1000 +- **Rating Range**: All ratings constrained to 1-5 range +- **URL Format**: No format validation at database level +- **Uniqueness**: Names are unique within their entity type + +## Relationships and Cascading + +### One-to-Many Relationships +- **Author → Stories**: Lazy loaded, cascade ALL operations +- **Series → Stories**: Lazy loaded, ordered by volume, cascade ALL +- **Author → Author URLs**: Eager loaded via `@ElementCollection` + +### Many-to-Many Relationships +- **Stories ↔ Tags**: Via `story_tags` junction table +- Managed bidirectionally with helper methods +- Cascade DELETE on both sides + +### Foreign Key Constraints +- All foreign keys have proper referential integrity +- DELETE operations cascade appropriately +- No orphaned records are allowed + +## Performance Considerations + +### Indexing Strategy +- Primary keys automatically indexed +- Foreign keys have dedicated indexes +- Frequently queried fields (rating, created_at) are indexed +- Unique constraints automatically create indexes + +### Query Optimization +- Lazy loading prevents N+1 queries +- Pagination used for large result sets +- Specialized queries for common access patterns +- Typesense search engine for full-text search (separate from PostgreSQL) + +### Data Volume Estimates +- **Stories**: Expected 1K-10K records per user +- **Authors**: Expected 100-1K records per user +- **Tags**: Expected 50-500 records per user +- **Series**: Expected 10-100 records per user +- **Join Tables**: Scale with story count and tagging usage + +## Backup and Migration Considerations + +### Schema Evolution +- Uses Hibernate `ddl-auto: update` for development +- Production should use controlled migration tools (Flyway/Liquibase) +- UUID keys allow safe data migration between environments + +### Data Integrity +- Foreign key constraints ensure referential integrity +- Check constraints validate data ranges +- Application-level validation provides user-friendly error messages +- Unique constraints prevent duplicate data + +### Backup Strategy +- Full PostgreSQL dumps for complete backup +- Image files stored separately in filesystem +- Consider incremental backups for large installations +- Test restore procedures regularly + +This data model provides a solid foundation for personal story library management with room for future enhancements while maintaining data integrity and performance. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..01fed91 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,147 @@ +# StoryCove Documentation + +Welcome to the StoryCove documentation! This directory contains comprehensive documentation for the StoryCove application. + +## 📚 Documentation Files + +### **[API Documentation](API.md)** +Complete REST API reference with detailed endpoint descriptions, request/response examples, authentication details, and error handling. + +**Contents:** +- Authentication endpoints +- Story management (CRUD, search, images) +- Author management (profiles, avatars, URLs) +- Tag system (creation, autocomplete, statistics) +- Series management (organization, navigation) +- File upload/download (images, validation) +- Search and indexing (Typesense integration) +- Configuration endpoints +- Data Transfer Objects (DTOs) +- Error handling and status codes + +### **[Data Model Documentation](DATA_MODEL.md)** +Detailed database schema documentation including entity relationships, constraints, indexes, and business rules. + +**Contents:** +- Entity Relationship Diagram +- Detailed table specifications +- Field types and constraints +- Relationship mappings +- Performance considerations +- Backup and migration strategy + +### **[OpenAPI Specification](openapi.yaml)** +Machine-readable API specification in OpenAPI 3.0 format for API testing tools, code generation, and integration. + +**Usage:** +- Import into Postman, Insomnia, or similar tools +- Generate client SDKs +- API documentation hosting (Swagger UI) +- Contract testing + +## 🛠️ How to Use This Documentation + +### **For Developers** +1. Start with the [API Documentation](API.md) for endpoint details +2. Reference [Data Model](DATA_MODEL.md) for database understanding +3. Use [OpenAPI spec](openapi.yaml) for testing and integration +4. See main [README](../README.md) for setup instructions + +### **For API Integration** +1. Review authentication flow in [API docs](API.md) +2. Import [OpenAPI spec](openapi.yaml) into your API client +3. Test endpoints using the provided examples +4. Implement error handling based on documented responses + +### **For Database Work** +1. Study [Data Model](DATA_MODEL.md) for schema understanding +2. Review relationship constraints and business rules +3. Use entity diagrams for system architecture planning +4. Follow migration guidelines for schema changes + +### **For System Administration** +1. Reference main [README](../README.md) for deployment instructions +2. Use environment configuration examples +3. Review backup strategies in [Data Model](DATA_MODEL.md) +4. Monitor API health using documented endpoints + +## 🔗 External Resources + +### **Related Documentation** +- **[Main README](../README.md)**: Project overview, setup, and deployment +- **[Technical Specification](../storycove-spec.md)**: Comprehensive technical specification +- **Environment Files**: `.env.example`, `.env.development`, `.env.production` + +### **Technology Documentation** +- **[Spring Boot](https://spring.io/projects/spring-boot)**: Backend framework +- **[Next.js](https://nextjs.org/)**: Frontend framework +- **[PostgreSQL](https://www.postgresql.org/docs/)**: Database system +- **[Typesense](https://typesense.org/docs/)**: Search engine +- **[Docker](https://docs.docker.com/)**: Containerization + +## 📝 Documentation Standards + +### **API Documentation** +- All endpoints documented with examples +- Request/response schemas included +- Error scenarios covered +- Authentication requirements specified + +### **Code Examples** +- JSON examples use realistic data +- UUIDs formatted correctly +- Timestamps in ISO 8601 format +- Error responses include helpful messages + +### **Data Model** +- All tables and relationships documented +- Constraints and validation rules specified +- Performance implications noted +- Migration considerations included + +## 🤝 Contributing to Documentation + +When updating the application: + +1. **API Changes**: Update [API.md](API.md) and [openapi.yaml](openapi.yaml) +2. **Database Changes**: Update [DATA_MODEL.md](DATA_MODEL.md) +3. **Feature Changes**: Update main [README](../README.md) features section +4. **Deployment Changes**: Update environment configuration examples + +### **Documentation Checklist** +- [ ] API endpoints documented with examples +- [ ] Database schema changes reflected +- [ ] OpenAPI specification updated +- [ ] Error handling documented +- [ ] Authentication requirements specified +- [ ] Performance implications noted +- [ ] Migration steps documented + +## 📋 Quick Reference + +### **Base URLs** +- **Development**: `http://localhost:6925/api` +- **Production**: `https://yourdomain.com/api` + +### **Authentication** +- **Method**: JWT tokens via httpOnly cookies +- **Login**: `POST /api/auth/login` +- **Token Expiry**: 24 hours + +### **Key Endpoints** +- **Stories**: `/api/stories` +- **Authors**: `/api/authors` +- **Tags**: `/api/tags` +- **Series**: `/api/series` +- **Search**: `/api/stories/search` +- **Files**: `/api/files` + +### **Common Response Codes** +- **200**: Success +- **201**: Created +- **400**: Bad Request +- **401**: Unauthorized +- **404**: Not Found +- **500**: Server Error + +This documentation is maintained alongside the codebase to ensure accuracy and completeness. For questions or clarifications, please refer to the appropriate documentation file or create an issue in the project repository. \ No newline at end of file diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..ca59945 --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,583 @@ +openapi: 3.0.3 +info: + title: StoryCove API + description: | + StoryCove is a self-hosted web application for storing, organizing, and reading short stories from various internet sources. + + ## Features + - Story management with HTML content support + - Author profiles with ratings and metadata + - Tag-based categorization and series organization + - Full-text search powered by Typesense + - Image upload for covers and avatars + - JWT-based authentication + + ## Authentication + All endpoints (except `/api/auth/login`) require JWT authentication via httpOnly cookies. + + version: 1.0.0 + contact: + name: StoryCove + license: + name: MIT + +servers: + - url: http://localhost:6925/api + description: Local development server + - url: https://storycove.hardegger.io/api + description: Production server + +security: + - cookieAuth: [] + +paths: + # Authentication endpoints + /auth/login: + post: + tags: [Authentication] + summary: Login with password + description: Authenticate with application password and receive JWT token + security: [] # No authentication required + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [password] + properties: + password: + type: string + description: Application password + responses: + '200': + description: Authentication successful + headers: + Set-Cookie: + description: JWT token cookie + schema: + type: string + example: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Max-Age=86400 + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Authentication successful + token: + type: string + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + '401': + $ref: '#/components/responses/Unauthorized' + + /auth/logout: + post: + tags: [Authentication] + summary: Logout and clear token + description: Clear authentication token and logout + responses: + '200': + description: Logout successful + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + + /auth/verify: + get: + tags: [Authentication] + summary: Verify token validity + description: Check if current JWT token is valid + responses: + '200': + description: Token is valid + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + '401': + $ref: '#/components/responses/Unauthorized' + + # Story endpoints + /stories: + get: + tags: [Stories] + summary: List all stories + description: Get paginated list of all stories + parameters: + - $ref: '#/components/parameters/PageParam' + - $ref: '#/components/parameters/SizeParam' + - $ref: '#/components/parameters/SortByParam' + - $ref: '#/components/parameters/SortDirParam' + responses: + '200': + description: Stories retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PagedStories' + '401': + $ref: '#/components/responses/Unauthorized' + + post: + tags: [Stories] + summary: Create new story + description: Create a new story with optional author, series, and tags + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateStoryRequest' + responses: + '201': + description: Story created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/StoryDto' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /stories/{id}: + get: + tags: [Stories] + summary: Get story by ID + description: Retrieve a specific story by its UUID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + description: Story UUID + responses: + '200': + description: Story retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/StoryDto' + '404': + $ref: '#/components/responses/NotFound' + '401': + $ref: '#/components/responses/Unauthorized' + + put: + tags: [Stories] + summary: Update story + description: Update an existing story + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateStoryRequest' + responses: + '200': + description: Story updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/StoryDto' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '401': + $ref: '#/components/responses/Unauthorized' + + delete: + tags: [Stories] + summary: Delete story + description: Delete a story and all its relationships + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Story deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + '404': + $ref: '#/components/responses/NotFound' + '401': + $ref: '#/components/responses/Unauthorized' + + /stories/search: + get: + tags: [Stories] + summary: Search stories + description: Search stories using Typesense full-text search + parameters: + - name: query + in: query + required: true + schema: + type: string + description: Search query + - $ref: '#/components/parameters/PageParam' + - $ref: '#/components/parameters/SizeParam' + - name: authors + in: query + schema: + type: array + items: + type: string + description: Filter by author names + - name: tags + in: query + schema: + type: array + items: + type: string + description: Filter by tag names + - name: minRating + in: query + schema: + type: integer + minimum: 1 + maximum: 5 + - name: maxRating + in: query + schema: + type: integer + minimum: 1 + maximum: 5 + responses: + '200': + description: Search results + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResult' + '401': + $ref: '#/components/responses/Unauthorized' + +components: + securitySchemes: + cookieAuth: + type: apiKey + in: cookie + name: token + + parameters: + PageParam: + name: page + in: query + schema: + type: integer + minimum: 0 + default: 0 + description: Page number (0-based) + + SizeParam: + name: size + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Page size + + SortByParam: + name: sortBy + in: query + schema: + type: string + default: createdAt + description: Field to sort by + + SortDirParam: + name: sortDir + in: query + schema: + type: string + enum: [asc, desc] + default: desc + description: Sort direction + + schemas: + StoryDto: + type: object + properties: + id: + type: string + format: uuid + title: + type: string + summary: + type: string + nullable: true + description: + type: string + nullable: true + contentHtml: + type: string + nullable: true + contentPlain: + type: string + nullable: true + sourceUrl: + type: string + nullable: true + coverPath: + type: string + nullable: true + wordCount: + type: integer + rating: + type: integer + minimum: 1 + maximum: 5 + nullable: true + volume: + type: integer + nullable: true + authorId: + type: string + format: uuid + nullable: true + authorName: + type: string + nullable: true + seriesId: + type: string + format: uuid + nullable: true + seriesName: + type: string + nullable: true + tags: + type: array + items: + $ref: '#/components/schemas/TagDto' + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + CreateStoryRequest: + type: object + required: [title] + properties: + title: + type: string + maxLength: 255 + summary: + type: string + description: + type: string + maxLength: 1000 + contentHtml: + type: string + sourceUrl: + type: string + volume: + type: integer + authorId: + type: string + format: uuid + authorName: + type: string + seriesId: + type: string + format: uuid + seriesName: + type: string + tagNames: + type: array + items: + type: string + + UpdateStoryRequest: + type: object + properties: + title: + type: string + maxLength: 255 + summary: + type: string + description: + type: string + maxLength: 1000 + contentHtml: + type: string + sourceUrl: + type: string + volume: + type: integer + authorId: + type: string + format: uuid + seriesId: + type: string + format: uuid + tagNames: + type: array + items: + type: string + + TagDto: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + storyCount: + type: integer + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + PagedStories: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/StoryDto' + pageable: + $ref: '#/components/schemas/Pageable' + totalElements: + type: integer + totalPages: + type: integer + last: + type: boolean + first: + type: boolean + numberOfElements: + type: integer + size: + type: integer + number: + type: integer + empty: + type: boolean + + Pageable: + type: object + properties: + sort: + $ref: '#/components/schemas/Sort' + pageNumber: + type: integer + pageSize: + type: integer + offset: + type: integer + paged: + type: boolean + unpaged: + type: boolean + + Sort: + type: object + properties: + sorted: + type: boolean + unsorted: + type: boolean + + SearchResult: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/StoryDto' + totalHits: + type: integer + format: int64 + page: + type: integer + perPage: + type: integer + query: + type: string + searchTimeMs: + type: integer + format: int64 + + MessageResponse: + type: object + properties: + message: + type: string + + ErrorResponse: + type: object + properties: + error: + type: string + details: + type: object + additionalProperties: + type: string + + responses: + BadRequest: + description: Bad request - validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + Unauthorized: + description: Authentication required or invalid token + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +tags: + - name: Authentication + description: Authentication endpoints + - name: Stories + description: Story management endpoints + - name: Authors + description: Author management endpoints + - name: Tags + description: Tag management endpoints + - name: Series + description: Series management endpoints + - name: Files + description: File upload and management endpoints + - name: Search + description: Search and indexing endpoints + - name: Configuration + description: Application configuration endpoints \ No newline at end of file