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