configurable url
This commit is contained in:
26
.env.development
Normal file
26
.env.development
Normal file
@@ -0,0 +1,26 @@
|
||||
# Development Environment Configuration
|
||||
# For local development with Docker
|
||||
|
||||
# Application URLs and CORS Configuration
|
||||
STORYCOVE_PUBLIC_URL=http://localhost:6925
|
||||
STORYCOVE_CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:6925
|
||||
|
||||
# Database Configuration
|
||||
DB_PASSWORD=dev_password
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=dev-jwt-secret-key-for-development-only
|
||||
|
||||
# Application Authentication
|
||||
APP_PASSWORD=admin
|
||||
|
||||
# Typesense Search Configuration
|
||||
TYPESENSE_API_KEY=dev_api_key
|
||||
TYPESENSE_ENABLED=true
|
||||
TYPESENSE_REINDEX_INTERVAL=3600000
|
||||
|
||||
# Image Storage
|
||||
IMAGE_STORAGE_PATH=/app/images
|
||||
|
||||
# Frontend API URL (for local development)
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080/api
|
||||
24
.env.example
24
.env.example
@@ -1,4 +1,26 @@
|
||||
# StoryCove Environment Configuration
|
||||
# Copy this file to .env and configure for your environment
|
||||
|
||||
# Application URLs and CORS Configuration
|
||||
STORYCOVE_PUBLIC_URL=http://localhost:6925
|
||||
STORYCOVE_CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:6925
|
||||
|
||||
# Database Configuration
|
||||
DB_PASSWORD=secure_password_here
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=secure_jwt_secret_here
|
||||
TYPESENSE_API_KEY=secure_api_key_here
|
||||
|
||||
# Application Authentication
|
||||
APP_PASSWORD=application_password_here
|
||||
|
||||
# Typesense Search Configuration
|
||||
TYPESENSE_API_KEY=secure_api_key_here
|
||||
TYPESENSE_ENABLED=true
|
||||
TYPESENSE_REINDEX_INTERVAL=3600000
|
||||
|
||||
# Image Storage
|
||||
IMAGE_STORAGE_PATH=/app/images
|
||||
|
||||
# Frontend API URL (for development only - not used in Docker)
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080/api
|
||||
30
.env.production
Normal file
30
.env.production
Normal file
@@ -0,0 +1,30 @@
|
||||
# Production Environment Configuration
|
||||
# Set these values in your production environment
|
||||
|
||||
# Application URLs and CORS Configuration
|
||||
# Replace with your actual production URL
|
||||
STORYCOVE_PUBLIC_URL=https://storycove.hardegger.io
|
||||
STORYCOVE_CORS_ALLOWED_ORIGINS=https://storycove.hardegger.io
|
||||
|
||||
# Database Configuration
|
||||
# Use a strong password in production
|
||||
DB_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
|
||||
# JWT Configuration
|
||||
# Use a strong, random secret in production
|
||||
JWT_SECRET=REPLACE_WITH_SECURE_JWT_SECRET_MINIMUM_32_CHARS
|
||||
|
||||
# Application Authentication
|
||||
# Use a strong password in production
|
||||
APP_PASSWORD=REPLACE_WITH_SECURE_APP_PASSWORD
|
||||
|
||||
# Typesense Search Configuration
|
||||
TYPESENSE_API_KEY=REPLACE_WITH_SECURE_TYPESENSE_API_KEY
|
||||
TYPESENSE_ENABLED=true
|
||||
TYPESENSE_REINDEX_INTERVAL=3600000
|
||||
|
||||
# Image Storage
|
||||
IMAGE_STORAGE_PATH=/app/images
|
||||
|
||||
# Frontend API URL (not used in Docker, but can be set for external deployments)
|
||||
NEXT_PUBLIC_API_URL=/api
|
||||
27
.env.staging
Normal file
27
.env.staging
Normal file
@@ -0,0 +1,27 @@
|
||||
# Staging Environment Configuration
|
||||
# For testing before production deployment
|
||||
|
||||
# Application URLs and CORS Configuration
|
||||
# Replace with your actual staging URL
|
||||
STORYCOVE_PUBLIC_URL=https://staging.yourdomain.com
|
||||
STORYCOVE_CORS_ALLOWED_ORIGINS=https://staging.yourdomain.com
|
||||
|
||||
# Database Configuration
|
||||
DB_PASSWORD=staging_secure_password
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=staging-jwt-secret-key-for-testing-32-chars-min
|
||||
|
||||
# Application Authentication
|
||||
APP_PASSWORD=staging_admin_password
|
||||
|
||||
# Typesense Search Configuration
|
||||
TYPESENSE_API_KEY=staging_typesense_api_key
|
||||
TYPESENSE_ENABLED=true
|
||||
TYPESENSE_REINDEX_INTERVAL=3600000
|
||||
|
||||
# Image Storage
|
||||
IMAGE_STORAGE_PATH=/app/images
|
||||
|
||||
# Frontend API URL
|
||||
NEXT_PUBLIC_API_URL=/api
|
||||
58
README.md
58
README.md
@@ -4,6 +4,24 @@ A self-hosted web application for storing, organizing, and reading short stories
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Using Environment-Specific Deployment (Recommended)
|
||||
|
||||
1. Deploy for your environment:
|
||||
```bash
|
||||
# For development
|
||||
./deploy.sh development
|
||||
|
||||
# For staging
|
||||
./deploy.sh staging
|
||||
|
||||
# For production (edit .env.production first with your values)
|
||||
./deploy.sh production
|
||||
```
|
||||
|
||||
2. Edit the environment file (`.env.development`, `.env.staging`, or `.env.production`) with your specific configuration
|
||||
|
||||
### Option 2: Manual Configuration
|
||||
|
||||
1. Copy environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
@@ -16,7 +34,7 @@ cp .env.example .env
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. Access the application at http://localhost
|
||||
4. Access the application at the URL specified in your environment configuration (default: http://localhost:6925)
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -26,6 +44,44 @@ docker-compose up -d
|
||||
- **Search**: Typesense (Port 8108)
|
||||
- **Proxy**: Nginx (Port 80)
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
StoryCove supports different deployment environments with specific configuration files:
|
||||
|
||||
### Environment Files
|
||||
|
||||
- `.env.development` - Local development with Docker
|
||||
- `.env.staging` - Staging/testing environment
|
||||
- `.env.production` - Production deployment
|
||||
- `.env.example` - Template with all available options
|
||||
|
||||
### Key Environment Variables
|
||||
|
||||
- `STORYCOVE_PUBLIC_URL` - The public URL where your app is accessible
|
||||
- `STORYCOVE_CORS_ALLOWED_ORIGINS` - Comma-separated list of allowed CORS origins
|
||||
- `DB_PASSWORD` - PostgreSQL database password
|
||||
- `JWT_SECRET` - Secret key for JWT token signing (minimum 32 characters)
|
||||
- `APP_PASSWORD` - Application login password
|
||||
- `TYPESENSE_API_KEY` - API key for Typesense search service
|
||||
- `NEXT_PUBLIC_API_URL` - Frontend API URL (for development only)
|
||||
|
||||
### Deployment Examples
|
||||
|
||||
For a production deployment at `https://stories.example.com`:
|
||||
```bash
|
||||
# Edit .env.production
|
||||
STORYCOVE_PUBLIC_URL=https://stories.example.com
|
||||
STORYCOVE_CORS_ALLOWED_ORIGINS=https://stories.example.com
|
||||
|
||||
# Deploy
|
||||
./deploy.sh production
|
||||
```
|
||||
|
||||
For local development:
|
||||
```bash
|
||||
./deploy.sh development
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Frontend Development
|
||||
|
||||
@@ -23,7 +23,7 @@ import java.util.List;
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Value("${storycove.cors.allowed-origins:${STORYCOVE_CORS_ALLOWED_ORIGINS:http://localhost:3000}}")
|
||||
@Value("${storycove.cors.allowed-origins}")
|
||||
private String allowedOrigins;
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@@ -23,10 +23,8 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -39,7 +37,6 @@ public class StoryController {
|
||||
private final StoryService storyService;
|
||||
private final AuthorService authorService;
|
||||
private final SeriesService seriesService;
|
||||
private final TagService tagService;
|
||||
private final HtmlSanitizationService sanitizationService;
|
||||
private final ImageService imageService;
|
||||
private final TypesenseService typesenseService;
|
||||
@@ -47,14 +44,12 @@ public class StoryController {
|
||||
public StoryController(StoryService storyService,
|
||||
AuthorService authorService,
|
||||
SeriesService seriesService,
|
||||
TagService tagService,
|
||||
HtmlSanitizationService sanitizationService,
|
||||
ImageService imageService,
|
||||
@Autowired(required = false) TypesenseService typesenseService) {
|
||||
this.storyService = storyService;
|
||||
this.authorService = authorService;
|
||||
this.seriesService = seriesService;
|
||||
this.tagService = tagService;
|
||||
this.sanitizationService = sanitizationService;
|
||||
this.imageService = imageService;
|
||||
this.typesenseService = typesenseService;
|
||||
@@ -88,7 +83,7 @@ public class StoryController {
|
||||
Story story = new Story();
|
||||
updateStoryFromRequest(story, request);
|
||||
|
||||
Story savedStory = storyService.create(story);
|
||||
Story savedStory = storyService.createWithTagNames(story, request.getTagNames());
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(convertToDto(savedStory));
|
||||
}
|
||||
|
||||
@@ -325,20 +320,7 @@ public class StoryController {
|
||||
story.setSeries(series);
|
||||
}
|
||||
|
||||
// Handle tags
|
||||
if (createReq.getTagNames() != null && !createReq.getTagNames().isEmpty()) {
|
||||
for (String tagName : createReq.getTagNames()) {
|
||||
if (tagName != null && !tagName.trim().isEmpty()) {
|
||||
Tag tag = tagService.findByNameOptional(tagName.trim().toLowerCase())
|
||||
.orElseGet(() -> {
|
||||
Tag newTag = new Tag();
|
||||
newTag.setName(tagName.trim().toLowerCase());
|
||||
return tagService.create(newTag);
|
||||
});
|
||||
story.addTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tags are handled in the service layer
|
||||
} else if (request instanceof UpdateStoryRequest updateReq) {
|
||||
if (updateReq.getTitle() != null) {
|
||||
story.setTitle(updateReq.getTitle());
|
||||
|
||||
@@ -338,7 +338,7 @@ public class AuthorService {
|
||||
if (updates.getAuthorRating() != null) {
|
||||
existing.setAuthorRating(updates.getAuthorRating());
|
||||
}
|
||||
if (updates.getUrls() != null && !updates.getUrls().isEmpty()) {
|
||||
if (updates.getUrls() != null) {
|
||||
existing.getUrls().clear();
|
||||
existing.getUrls().addAll(updates.getUrls());
|
||||
}
|
||||
|
||||
@@ -316,6 +316,36 @@ public class StoryService {
|
||||
return savedStory;
|
||||
}
|
||||
|
||||
public Story createWithTagNames(@Valid Story story, java.util.List<String> tagNames) {
|
||||
validateStoryForCreate(story);
|
||||
|
||||
// Set up relationships
|
||||
if (story.getAuthor() != null && story.getAuthor().getId() != null) {
|
||||
Author author = authorService.findById(story.getAuthor().getId());
|
||||
story.setAuthor(author);
|
||||
}
|
||||
|
||||
if (story.getSeries() != null && story.getSeries().getId() != null) {
|
||||
Series series = seriesService.findById(story.getSeries().getId());
|
||||
story.setSeries(series);
|
||||
validateSeriesVolume(series, story.getVolume());
|
||||
}
|
||||
|
||||
Story savedStory = storyRepository.save(story);
|
||||
|
||||
// Handle tags by names
|
||||
if (tagNames != null && !tagNames.isEmpty()) {
|
||||
updateStoryTagsByNames(savedStory, tagNames);
|
||||
}
|
||||
|
||||
// Index in Typesense (if available)
|
||||
if (typesenseService != null) {
|
||||
typesenseService.indexStory(savedStory);
|
||||
}
|
||||
|
||||
return savedStory;
|
||||
}
|
||||
|
||||
public Story update(UUID id, @Valid Story storyUpdates) {
|
||||
Story existingStory = findById(id);
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ server:
|
||||
port: 8080
|
||||
|
||||
storycove:
|
||||
app:
|
||||
public-url: ${STORYCOVE_PUBLIC_URL:http://localhost:6925}
|
||||
cors:
|
||||
allowed-origins: ${STORYCOVE_CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:6925}
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:default-secret-key}
|
||||
expiration: 86400000 # 24 hours
|
||||
|
||||
35
deploy.sh
Executable file
35
deploy.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# StoryCove Deployment Script
|
||||
# Usage: ./deploy.sh [environment]
|
||||
# Environments: development, staging, production
|
||||
|
||||
set -e
|
||||
|
||||
ENVIRONMENT=${1:-development}
|
||||
ENV_FILE=".env.${ENVIRONMENT}"
|
||||
|
||||
echo "Deploying StoryCove for ${ENVIRONMENT} environment..."
|
||||
|
||||
# Check if environment file exists
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Error: Environment file $ENV_FILE not found."
|
||||
echo "Available environments: development, staging, production"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy environment file to .env
|
||||
cp "$ENV_FILE" .env
|
||||
echo "Using environment configuration from $ENV_FILE"
|
||||
|
||||
# Build and start services
|
||||
echo "Building and starting Docker services..."
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
|
||||
echo "Deployment complete!"
|
||||
echo "StoryCove is running at: $(grep STORYCOVE_PUBLIC_URL $ENV_FILE | cut -d'=' -f2)"
|
||||
echo ""
|
||||
echo "To view logs: docker-compose logs -f"
|
||||
echo "To stop: docker-compose down"
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
frontend:
|
||||
build: ./frontend
|
||||
environment:
|
||||
- NEXT_PUBLIC_API_URL=http://backend:8080/api
|
||||
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-/api}
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
- TYPESENSE_PORT=8108
|
||||
- IMAGE_STORAGE_PATH=/app/images
|
||||
- APP_PASSWORD=${APP_PASSWORD}
|
||||
- STORYCOVE_CORS_ALLOWED_ORIGINS=https://storycove.sharyavin.synology.me,http://localhost:3000
|
||||
- STORYCOVE_CORS_ALLOWED_ORIGINS=${STORYCOVE_CORS_ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:6925}
|
||||
volumes:
|
||||
- images_data:/app/images
|
||||
depends_on:
|
||||
|
||||
4
frontend/.env.development
Normal file
4
frontend/.env.development
Normal file
@@ -0,0 +1,4 @@
|
||||
# Frontend Development Environment
|
||||
# For local development outside of Docker
|
||||
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080/api
|
||||
4
frontend/.env.production
Normal file
4
frontend/.env.production
Normal file
@@ -0,0 +1,4 @@
|
||||
# Frontend Production Environment
|
||||
# API URL should be relative in Docker setup
|
||||
|
||||
NEXT_PUBLIC_API_URL=/api
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { authApi } from '../lib/api';
|
||||
import { preloadSanitizationConfig } from '../lib/sanitization';
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
@@ -30,7 +31,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Preload sanitization config for content formatting
|
||||
const loadSanitizationConfig = async () => {
|
||||
try {
|
||||
await preloadSanitizationConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to preload sanitization config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
loadSanitizationConfig();
|
||||
}, []);
|
||||
|
||||
const login = async (password: string) => {
|
||||
|
||||
Reference in New Issue
Block a user