Intial Setup

This commit is contained in:
Stefan Hardegger
2025-07-21 08:47:52 +02:00
commit 68c7c8115f
20 changed files with 1276 additions and 0 deletions

4
.env.example Normal file
View File

@@ -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

49
.gitignore vendored Normal file
View File

@@ -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/

91
CLAUDE.md Normal file
View File

@@ -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
```

60
README.md Normal file
View File

@@ -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`.

16
backend/Dockerfile Normal file
View File

@@ -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"]

102
backend/pom.xml Normal file
View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.storycove</groupId>
<artifactId>storycove-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>storycove-backend</name>
<description>StoryCove Backend API</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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);
}
}

View File

@@ -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

60
docker-compose.yml Normal file
View File

@@ -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:

13
frontend/Dockerfile Normal file
View File

@@ -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"]

19
frontend/next.config.js Normal file
View File

@@ -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;

32
frontend/package.json Normal file
View File

@@ -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"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -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;
}
}

60
frontend/src/types/api.ts Normal file
View File

@@ -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;
}

View File

@@ -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',
};

28
frontend/tsconfig.json Normal file
View File

@@ -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"]
}

50
nginx.conf Normal file
View File

@@ -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";
}
}
}

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "StoryCove",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

571
storycove-spec.md Normal file
View File

@@ -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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
```
#### 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