commit 68c7c8115f2a33f071eaad500e635e73c6a69bb2 Author: Stefan Hardegger Date: Mon Jul 21 08:47:52 2025 +0200 Intial Setup diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a3ddc2d --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +DB_PASSWORD=secure_password_here +JWT_SECRET=secure_jwt_secret_here +TYPESENSE_API_KEY=secure_api_key_here +APP_PASSWORD=application_password_here \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9612499 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Next.js +.next/ +out/ +dist/ + +# Java / Maven +target/ +*.jar +*.war +*.ear +*.logs +.mvn/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Docker +.docker/ + +# Application data +images/ +data/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1bcccf8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +StoryCove is a self-hosted web application for storing, organizing, and reading short stories from internet sources. The project is currently in the specification phase with a comprehensive requirements document. + +## Architecture + +This is a multi-tier application with the following planned stack: +- **Frontend**: Next.js +- **Backend**: Spring Boot (Java) +- **Database**: PostgreSQL +- **Search**: Typesense +- **Reverse Proxy**: Nginx +- **Deployment**: Docker & Docker Compose + +## Development Commands + +### Frontend (Next.js) +- `cd frontend && npm run dev` - Start development server +- `cd frontend && npm run build` - Build for production +- `cd frontend && npm run lint` - Run ESLint +- `cd frontend && npm run type-check` - Run TypeScript compiler + +### Backend (Spring Boot) +- `cd backend && ./mvnw spring-boot:run` - Start development server +- `cd backend && ./mvnw test` - Run tests +- `cd backend && ./mvnw clean package` - Build JAR + +### Docker +- `docker-compose up -d` - Start all services +- `docker-compose down` - Stop all services +- `docker-compose build` - Rebuild containers +- `docker-compose logs -f [service]` - View logs + +## Current Project State + +The repository now has a complete project structure with Docker configuration, frontend Next.js setup, and backend Spring Boot foundation ready for development. + +## Key Implementation Details from Specification + +### Database Schema +- Stories with HTML content, metadata, ratings, and file attachments +- Authors with profiles, ratings, and URL collections +- Tag-based categorization system +- Series support for multi-part stories +- UUID-based primary keys throughout + +### API Design +- RESTful endpoints for stories, authors, tags, and series +- JWT-based authentication with single password +- Multipart form data for file uploads (covers, avatars) +- Full-text search integration with Typesense + +### Security Requirements +- HTML sanitization using allowlist (Jsoup on backend) +- Content Security Policy headers +- Input validation and parameterized queries +- Image processing with size limits and format restrictions + +### Image Handling +- Cover images for stories (800x1200 max) +- Avatar images for authors (400x400) +- File storage in Docker volumes +- Support for JPG, PNG, WebP formats + +## Development Tasks + +Based on the specification, implementation should follow this sequence: +1. Docker environment and database setup +2. Spring Boot backend with core CRUD operations +3. Typesense search integration +4. Next.js frontend with authentication +5. Reading interface and user experience features + +## Authentication Flow +- Single password authentication (no user accounts) +- JWT tokens stored in httpOnly cookies +- Protected routes via middleware + +## File Structure Expectations +When implementation begins, the structure should follow: +``` +frontend/ # Next.js application +backend/ # Spring Boot application +nginx.conf # Reverse proxy configuration +docker-compose.yml # Container orchestration +.env # Environment variables +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..70ba340 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# StoryCove + +A self-hosted web application for storing, organizing, and reading short stories from various internet sources. + +## Quick Start + +1. Copy environment variables: +```bash +cp .env.example .env +``` + +2. Edit `.env` with secure values for all variables + +3. Start the application: +```bash +docker-compose up -d +``` + +4. Access the application at http://localhost + +## Architecture + +- **Frontend**: Next.js (Port 3000) +- **Backend**: Spring Boot (Port 8080) +- **Database**: PostgreSQL (Port 5432) +- **Search**: Typesense (Port 8108) +- **Proxy**: Nginx (Port 80) + +## Development + +### Frontend Development +```bash +cd frontend +npm install +npm run dev +``` + +### Backend Development +```bash +cd backend +./mvnw spring-boot:run +``` + +### Commands +- `docker-compose up -d` - Start all services +- `docker-compose down` - Stop all services +- `docker-compose logs -f [service]` - View logs +- `docker-compose build` - Rebuild containers + +## 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 + +For detailed specifications, see `storycove-spec.md`. \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..e877e8c --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,16 @@ +FROM openjdk:17-jdk-slim + +WORKDIR /app + +COPY pom.xml . +COPY src ./src + +RUN apt-get update && apt-get install -y maven && \ + mvn clean package -DskipTests && \ + apt-get remove -y maven && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +EXPOSE 8080 + +CMD ["java", "-jar", "target/storycove-backend-0.0.1-SNAPSHOT.jar"] \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..2592a49 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.storycove + storycove-backend + 0.0.1-SNAPSHOT + storycove-backend + StoryCove Backend API + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.postgresql + postgresql + runtime + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + org.jsoup + jsoup + 1.17.1 + + + org.apache.httpcomponents.client5 + httpclient5 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/backend/src/main/java/com/storycove/StoryCoveApplication.java b/backend/src/main/java/com/storycove/StoryCoveApplication.java new file mode 100644 index 0000000..0ce5fa4 --- /dev/null +++ b/backend/src/main/java/com/storycove/StoryCoveApplication.java @@ -0,0 +1,13 @@ +package com.storycove; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StoryCoveApplication { + + public static void main(String[] args) { + SpringApplication.run(StoryCoveApplication.class, args); + } + +} \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..f18e358 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,41 @@ +spring: + datasource: + url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/storycove} + username: ${SPRING_DATASOURCE_USERNAME:storycove} + password: ${SPRING_DATASOURCE_PASSWORD:password} + driver-class-name: org.postgresql.Driver + + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + show-sql: false + + servlet: + multipart: + max-file-size: 5MB + max-request-size: 10MB + +server: + port: 8080 + +storycove: + jwt: + secret: ${JWT_SECRET:default-secret-key} + expiration: 86400000 # 24 hours + auth: + password: ${APP_PASSWORD:admin} + typesense: + api-key: ${TYPESENSE_API_KEY:xyz} + host: ${TYPESENSE_HOST:localhost} + port: ${TYPESENSE_PORT:8108} + images: + storage-path: ${IMAGE_STORAGE_PATH:/app/images} + +logging: + level: + com.storycove: DEBUG + org.springframework.security: DEBUG \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b037a9c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3.8' + +services: + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - images_data:/app/images:ro + depends_on: + - frontend + - backend + + frontend: + build: ./frontend + environment: + - NEXT_PUBLIC_API_URL=http://backend:8080 + depends_on: + - backend + + backend: + build: ./backend + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/storycove + - SPRING_DATASOURCE_USERNAME=storycove + - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - TYPESENSE_API_KEY=${TYPESENSE_API_KEY} + - TYPESENSE_HOST=typesense + - TYPESENSE_PORT=8108 + - IMAGE_STORAGE_PATH=/app/images + - APP_PASSWORD=${APP_PASSWORD} + volumes: + - images_data:/app/images + depends_on: + - postgres + - typesense + + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_DB=storycove + - POSTGRES_USER=storycove + - POSTGRES_PASSWORD=${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + + typesense: + image: typesense/typesense:0.25.0 + environment: + - TYPESENSE_API_KEY=${TYPESENSE_API_KEY} + - TYPESENSE_DATA_DIR=/data + volumes: + - typesense_data:/data + +volumes: + postgres_data: + typesense_data: + images_data: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b55a8a8 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . +RUN npm run build + +EXPOSE 3000 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..9c788ab --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,19 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + appDir: true, + }, + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`, + }, + ]; + }, + images: { + domains: ['localhost'], + }, +}; + +module.exports = nextConfig; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..eaf499a --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "storycove-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "14.0.0", + "react": "^18", + "react-dom": "^18", + "axios": "^1.6.0", + "dompurify": "^3.0.5", + "react-dropzone": "^14.2.3", + "tailwindcss": "^3.3.0", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.31" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/dompurify": "^3.0.5", + "eslint": "^8", + "eslint-config-next": "14.0.0" + } +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..8567b4c --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..0f089aa --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,34 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + font-family: Inter, system-ui, sans-serif; + } +} + +@layer components { + .reading-content { + @apply max-w-reading mx-auto px-6 py-8; + font-family: Georgia, Times, serif; + line-height: 1.7; + } + + .reading-content h1, + .reading-content h2, + .reading-content h3, + .reading-content h4, + .reading-content h5, + .reading-content h6 { + @apply font-bold mt-8 mb-4; + } + + .reading-content p { + @apply mb-4; + } + + .reading-content blockquote { + @apply border-l-4 border-gray-300 pl-4 italic my-6; + } +} \ No newline at end of file diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..867b9ae --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,60 @@ +export interface Story { + id: string; + title: string; + authorId: string; + authorName: string; + contentHtml: string; + contentPlain: string; + sourceUrl?: string; + wordCount: number; + seriesId?: string; + seriesName?: string; + volume?: number; + rating?: number; + coverImagePath?: string; + tags: string[]; + createdAt: string; + updatedAt: string; +} + +export interface Author { + id: string; + name: string; + notes?: string; + authorRating?: number; + avatarImagePath?: string; + urls: AuthorUrl[]; + stories: Story[]; + createdAt: string; + updatedAt: string; +} + +export interface AuthorUrl { + id: string; + url: string; + description?: string; +} + +export interface Tag { + id: string; + name: string; + storyCount: number; +} + +export interface Series { + id: string; + name: string; + storyCount: number; +} + +export interface AuthResponse { + token: string; + expiresIn: number; +} + +export interface SearchResult { + stories: Story[]; + totalCount: number; + page: number; + totalPages: number; +} \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..300fb21 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,21 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + fontFamily: { + serif: ['Georgia', 'Times', 'serif'], + sans: ['Inter', 'system-ui', 'sans-serif'], + }, + maxWidth: { + 'reading': '800px', + }, + }, + }, + plugins: [], + darkMode: 'class', +}; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..b1814a0 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "es6"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..c327ac4 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,50 @@ +events { + worker_connections 1024; +} + +http { + upstream frontend { + server frontend:3000; + } + + upstream backend { + server backend:8080; + } + + server { + listen 80; + client_max_body_size 10M; + + # Frontend routes + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Backend API routes + location /api/ { + proxy_pass http://backend/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Static image serving + location /images/ { + alias /app/images/; + expires 1y; + add_header Cache-Control "public, immutable"; + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..006a4de --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "StoryCove", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/storycove-spec.md b/storycove-spec.md new file mode 100644 index 0000000..7f7e4d6 --- /dev/null +++ b/storycove-spec.md @@ -0,0 +1,571 @@ +# StoryCove - Software Requirements Specification + +## 1. Executive Summary + +StoryCove is a self-hosted web application designed to store, organize, and read short stories collected from various internet sources. The application provides a clean, responsive interface for managing a personal library of stories with advanced search capabilities, author management, and rating systems. + +### 1.1 Key Features (MVP) +- 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 + +### 1.2 Technology Stack +- **Frontend**: Next.js +- **Backend**: Spring Boot (Java) +- **Database**: PostgreSQL +- **Search Engine**: Typesense +- **Reverse Proxy**: Nginx +- **Containerization**: Docker & Docker Compose + +## 2. System Architecture + +### 2.1 Container Architecture +``` +┌─────────────────────────────────────────────────────────────┐ +│ Nginx (Port 80/443) │ +│ Reverse Proxy │ +└─────────────┬───────────────────────────┬───────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────────────┐ +│ Next.js Frontend │ │ Spring Boot Backend │ +│ (Port 3000) │ │ (Port 8080) │ +└─────────────────────────┘ └──────────┬──────────┬──────────┘ + │ │ + ▼ ▼ + ┌────────────────┐ ┌─────────────────┐ + │ PostgreSQL │ │ Typesense │ + │ (Port 5432) │ │ (Port 8108) │ + └────────────────┘ └─────────────────┘ +``` + +### 2.2 Data Flow +1. User accesses application through Nginx +2. Nginx routes requests to appropriate service +3. Frontend communicates with Backend API +4. Backend manages data in PostgreSQL and syncs search index with Typesense +5. Authentication handled via JWT tokens + +## 3. Data Models + +### 3.1 Database Schema + +#### Story Table +```sql +CREATE TABLE stories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(500) NOT NULL, + author_id UUID NOT NULL, + content_html TEXT NOT NULL, + content_plain TEXT NOT NULL, + source_url VARCHAR(1000), + word_count INTEGER NOT NULL, + series_id UUID, + volume INTEGER, + rating INTEGER CHECK (rating >= 1 AND rating <= 5), + cover_image_path VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (author_id) REFERENCES authors(id), + FOREIGN KEY (series_id) REFERENCES series(id) +); +``` + +#### Author Table +```sql +CREATE TABLE authors ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL UNIQUE, + notes TEXT, + author_rating INTEGER CHECK (author_rating >= 1 AND author_rating <= 5), + avatar_image_path VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +#### Author URLs Table +```sql +CREATE TABLE author_urls ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + author_id UUID NOT NULL, + url VARCHAR(1000) NOT NULL, + description VARCHAR(255), + FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE +); +``` + +#### Series Table +```sql +CREATE TABLE series ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(500) NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +#### Tags Table +```sql +CREATE TABLE tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +#### Story Tags Junction Table +```sql +CREATE TABLE story_tags ( + story_id UUID NOT NULL, + tag_id UUID NOT NULL, + PRIMARY KEY (story_id, tag_id), + FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); +``` + +### 3.2 Typesense Schema + +```json +{ + "name": "stories", + "fields": [ + {"name": "id", "type": "string"}, + {"name": "title", "type": "string"}, + {"name": "author_name", "type": "string"}, + {"name": "content", "type": "string"}, + {"name": "tags", "type": "string[]"}, + {"name": "series_name", "type": "string", "optional": true}, + {"name": "word_count", "type": "int32"}, + {"name": "rating", "type": "int32", "optional": true}, + {"name": "created_at", "type": "int64"} + ], + "default_sorting_field": "created_at" +} +``` + +## 4. API Specification + +### 4.1 Authentication Endpoints + +#### POST /api/auth/login +```json +Request: +{ + "password": "string" +} + +Response: +{ + "token": "jwt_token_string", + "expiresIn": 86400 +} +``` + +### 4.2 Story Endpoints + +#### GET /api/stories +Query parameters: +- `page` (integer): Page number +- `limit` (integer): Items per page +- `search` (string): Search query +- `tags` (string[]): Filter by tags +- `authorId` (uuid): Filter by author +- `seriesId` (uuid): Filter by series + +#### POST /api/stories +```json +Request (multipart/form-data): +{ + "title": "string", + "authorName": "string", + "contentHtml": "string", + "sourceUrl": "string", + "tags": ["string"], + "seriesName": "string", + "volume": "integer", + "coverImage": "file (optional)" +} +``` + +#### POST /api/stories/{id}/cover +Upload/update cover image +``` +Request: multipart/form-data +- coverImage: file + +Response: +{ + "imagePath": "string" +} +``` + +#### GET /api/stories/{id} +Returns full story details including HTML content + +#### PUT /api/stories/{id} +Update story (same structure as POST) + +#### DELETE /api/stories/{id} +Delete a story + +#### PUT /api/stories/{id}/rating +```json +Request: +{ + "rating": 1-5 +} +``` + +### 4.3 Author Endpoints + +#### GET /api/authors +List all authors with story count and average rating + +#### GET /api/authors/{id} +Get author details with all stories + +#### PUT /api/authors/{id} +```json +Request (multipart/form-data): +{ + "notes": "string", + "authorRating": 1-5, + "urls": [ + { + "url": "string", + "description": "string" + } + ], + "avatarImage": "file (optional)" +} +``` + +#### POST /api/authors/{id}/avatar +Upload/update author avatar +``` +Request: multipart/form-data +- avatarImage: file + +Response: +{ + "imagePath": "string" +} +``` + +### 4.4 Image Endpoints + +#### GET /api/images/{type}/{filename} +Serve images (covers or avatars) +- type: "covers" or "avatars" +- filename: stored filename + +#### DELETE /api/stories/{id}/cover +Remove cover image from story + +#### DELETE /api/authors/{id}/avatar +Remove avatar from author + +### 4.4 Tag Endpoints + +#### GET /api/tags +Returns tag cloud data with usage counts + +#### GET /api/tags/autocomplete?q={query} +Autocomplete for tag input + +### 4.5 Series Endpoints + +#### GET /api/series +List all series with story counts + +#### GET /api/series/{id}/stories +Get all stories in a series ordered by volume + +## 5. UI/UX Specifications + +### 5.1 Main Views + +#### Story List View +- Grid/List toggle +- Search bar with real-time results +- Tag cloud for filtering +- Sort options: Date added, Title, Author, Rating +- Pagination +- Cover image thumbnails in grid view +- Quick actions: Edit, Delete, Read + +#### Story Create/Edit Form +- Title input (required) +- Author input with autocomplete +- Cover image upload with preview +- Rich text editor for content (with HTML source view) +- Tag input with autocomplete and chip display +- Series dropdown with "Add new" option +- Volume number input (shown if series selected) +- Source URL input +- Save/Cancel buttons + +#### Reading View +- Clean, distraction-free interface +- Cover image display at the top (if available) +- Responsive typography +- Progress indicator +- Navigation: Previous/Next in series +- Quick access to rate story +- Back to library button + +#### Author View +- Author avatar display +- Author name and rating display +- Editable notes section +- URL links management +- Story list with cover thumbnails and individual ratings +- Average story rating calculation +- Edit author details button with avatar upload + +### 5.2 Global Settings +- Font family selection +- Font size adjustment +- Theme selection (Light/Dark) +- Reading width preference + +## 6. Technical Implementation Details + +### 6.1 Frontend (Next.js) + +#### Key Libraries +- **UI Framework**: Tailwind CSS or Material-UI +- **State Management**: React Context or Zustand +- **HTTP Client**: Axios or Fetch API +- **HTML Sanitization**: DOMPurify +- **Rich Text Editor**: TinyMCE or Quill +- **Image Handling**: react-dropzone for uploads + +#### Authentication Flow +```typescript +// JWT stored in httpOnly cookie +// Auth context provides user state +// Protected routes using middleware +``` + +#### Image Upload Handling +```typescript +// Use multipart/form-data for story and author forms +// Image preview before upload +// Supported formats: JPG, PNG, WebP +// Max file size: 5MB +// Automatic image optimization on backend +``` + +### 6.2 Backend (Spring Boot) + +#### Key Dependencies +```xml + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt + + + org.jsoup + jsoup + + + org.postgresql + postgresql + + +``` + +#### HTML Processing +```java +// Use Jsoup for HTML sanitization +// Whitelist allowed tags: p, br, strong, em, ul, ol, li, h1-h6, blockquote +// Strip all JavaScript and unsafe attributes +// Generate plain text version for search indexing +``` + +#### Image Processing +```java +// Image upload handling +// Supported formats: JPG, PNG, WebP +// Max size: 5MB +// Automatic resizing: covers to 800x1200 max, avatars to 400x400 +// Store in filesystem: /app/images/covers/ and /app/images/avatars/ +// Generate unique filenames using UUID +// Thumbnail generation for list views +``` + +### 6.3 Search Integration + +#### Typesense Sync Strategy +1. On story create/update: Index immediately +2. On story delete: Remove from index +3. Batch reindex endpoint for maintenance +4. Search includes: title, author, content, tags + +### 6.4 Security Considerations + +1. **Authentication**: JWT with secure httpOnly cookies +2. **Input Validation**: All inputs validated and sanitized +3. **HTML Sanitization**: Strict whitelist of allowed tags +4. **SQL Injection**: Use parameterized queries +5. **XSS Prevention**: Content Security Policy headers +6. **CORS**: Configured for frontend origin only + +## 7. Deployment Configuration + +### 7.1 Docker Compose Configuration + +```yaml +version: '3.8' + +services: + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - images_data:/app/images:ro + depends_on: + - frontend + - backend + + frontend: + build: ./frontend + environment: + - NEXT_PUBLIC_API_URL=http://backend:8080 + depends_on: + - backend + + backend: + build: ./backend + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/storycove + - SPRING_DATASOURCE_USERNAME=storycove + - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - TYPESENSE_API_KEY=${TYPESENSE_API_KEY} + - TYPESENSE_HOST=typesense + - TYPESENSE_PORT=8108 + - IMAGE_STORAGE_PATH=/app/images + volumes: + - images_data:/app/images + depends_on: + - postgres + - typesense + + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_DB=storycove + - POSTGRES_USER=storycove + - POSTGRES_PASSWORD=${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + + typesense: + image: typesense/typesense:0.25.0 + environment: + - TYPESENSE_API_KEY=${TYPESENSE_API_KEY} + - TYPESENSE_DATA_DIR=/data + volumes: + - typesense_data:/data + +volumes: + postgres_data: + typesense_data: + images_data: +``` + +### 7.2 Environment Variables +```env +DB_PASSWORD=secure_password_here +JWT_SECRET=secure_jwt_secret_here +TYPESENSE_API_KEY=secure_api_key_here +APP_PASSWORD=application_password_here +``` + +## 8. Testing Strategy + +### 8.1 Unit Tests +- Backend: JUnit 5 for service and controller tests +- Frontend: Jest and React Testing Library + +### 8.2 Integration Tests +- API endpoint testing with MockMvc +- Database integration tests with Testcontainers + +### 8.3 E2E Tests +- Cypress or Playwright for critical user flows + +## 9. Phase 2 Roadmap + +### 9.1 URL Content Grabbing +- Configurable scrapers for specific sites +- Site configuration stored in database +- Content extraction rules per site +- Image download and storage + +### 9.2 Image Support +- Image storage in filesystem or S3-compatible storage +- Image optimization pipeline +- Inline image display in stories + +### 9.3 Story Collections +- Collection management interface +- Ordered story lists +- Collection sharing (future) + +### 9.4 Export Functionality +- PDF generation with formatting +- EPUB export with metadata +- Batch export for collections + +## 10. Development Milestones + +### Milestone 1: Infrastructure Setup (Week 1) +- Docker environment configuration +- Database schema implementation +- Basic Spring Boot setup with security + +### Milestone 2: Core Backend (Week 2-3) +- Story CRUD operations +- Author management +- Tag system +- Typesense integration + +### Milestone 3: Frontend Foundation (Week 4-5) +- Authentication flow +- Story list and create forms +- Author views +- Search interface + +### Milestone 4: Reading Experience (Week 6) +- Reading view implementation +- Settings management +- Rating system + +### Milestone 5: Polish & Testing (Week 7-8) +- UI refinements +- Comprehensive testing +- Documentation +- Deployment scripts \ No newline at end of file