Intial Setup
This commit is contained in:
4
.env.example
Normal file
4
.env.example
Normal 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
49
.gitignore
vendored
Normal 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
91
CLAUDE.md
Normal 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
60
README.md
Normal 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
16
backend/Dockerfile
Normal 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
102
backend/pom.xml
Normal 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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
backend/src/main/resources/application.yml
Normal file
41
backend/src/main/resources/application.yml
Normal 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
60
docker-compose.yml
Normal 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
13
frontend/Dockerfile
Normal 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
19
frontend/next.config.js
Normal 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
32
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
34
frontend/src/app/globals.css
Normal file
34
frontend/src/app/globals.css
Normal 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
60
frontend/src/types/api.ts
Normal 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;
|
||||||
|
}
|
||||||
21
frontend/tailwind.config.js
Normal file
21
frontend/tailwind.config.js
Normal 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
28
frontend/tsconfig.json
Normal 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
50
nginx.conf
Normal 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
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "StoryCove",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
571
storycove-spec.md
Normal file
571
storycove-spec.md
Normal 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
|
||||||
Reference in New Issue
Block a user