diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de256ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,145 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Build outputs +dist/ +build/ +.vite/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage +.coverage +.nyc_output +coverage/ + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Docker +.dockerignore + +# Production +.env.production +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..72b0230 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,11 @@ +# FastAPI Configuration +HOST=0.0.0.0 +PORT=8000 +DEBUG=false + +# CORS Settings +CORS_ORIGINS=https://slow-reader.velouria.dev + +# Application Settings +APP_NAME=Slow Reader API +VERSION=1.0.0 \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..a1e3e93 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,30 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY pyproject.toml ./ +COPY backend/ ./backend/ + +# Install uv +RUN pip install uv + +# Install dependencies +RUN uv pip install --system -r pyproject.toml + +# Download NLTK data +RUN python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab')" + +# Create non-root user +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + +EXPOSE 8000 + +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cc62ef6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile + restart: always + networks: + - default + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.http.routers.slow-reader-api.rule=Host(`slow-reader.velouria.dev`) && (PathPrefix(`/api/`) || PathPrefix(`/ws/`))" + - "traefik.http.routers.slow-reader-api.entrypoints=websecure" + - "traefik.http.routers.slow-reader-api.tls.certresolver=myresolver" + - "traefik.http.services.slow-reader-api.loadbalancer.server.port=8000" + - "traefik.docker.network=traefik_network" + - "homepage.group=Tools" + - "homepage.name=Slow Reader API" + - "homepage.description=Reading Focus API" + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + restart: always + depends_on: + - backend + networks: + - default + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.http.routers.slow-reader.rule=Host(`slow-reader.velouria.dev`)" + - "traefik.http.routers.slow-reader.entrypoints=websecure" + - "traefik.http.routers.slow-reader.tls.certresolver=myresolver" + - "traefik.http.services.slow-reader.loadbalancer.server.port=80" + - "traefik.docker.network=traefik_network" + - "homepage.group=Tools" + - "homepage.name=Slow Reader" + - "homepage.description=Focused Reading Experience" + +networks: + default: + name: slow-reader_default + traefik_network: + external: true \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..7440304 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM node:18-alpine as build + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built assets from build stage +COPY --from=build /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Create non-root user +RUN addgroup -g 1001 -S appuser && \ + adduser -S appuser -u 1001 -G appuser && \ + chown -R appuser:appuser /usr/share/nginx/html && \ + chown -R appuser:appuser /var/cache/nginx && \ + chown -R appuser:appuser /var/log/nginx && \ + chown -R appuser:appuser /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R appuser:appuser /var/run/nginx.pid + +USER appuser + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..2569b93 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline' 'unsafe-eval'" always; + + # Handle client-side routing + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Cache HTML with shorter expiry + location ~* \.html$ { + expires 1h; + add_header Cache-Control "public"; + } +} \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4e14851..28698c4 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -299,7 +299,11 @@ That's what Slow Reader is about. Just you and the text, moving at a pace that l }, connectWebSocket() { - const wsUrl = `ws://localhost:8000/ws/reading-session` + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const host = window.location.host + const wsUrl = import.meta.env.DEV + ? `ws://localhost:8000/ws/reading-session` + : `${protocol}//${host}/ws/reading-session` this.websocket = new WebSocket(wsUrl) this.websocket.onopen = () => { @@ -556,7 +560,8 @@ That's what Slow Reader is about. Just you and the text, moving at a pace that l this.extractedArticle = null try { - const response = await fetch('/api/extract-article', { + const apiBase = import.meta.env.DEV ? '/api' : '/api' + const response = await fetch(`${apiBase}/extract-article`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -633,7 +638,8 @@ That's what Slow Reader is about. Just you and the text, moving at a pace that l } try { - const response = await fetch('/api/analyze-text', { + const apiBase = import.meta.env.DEV ? '/api' : '/api' + const response = await fetch(`${apiBase}/analyze-text`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/frontend/vite.config.js b/frontend/vite.config.js index ebf7e76..bf697b8 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -17,6 +17,19 @@ export default defineConfig({ } } }, + build: { + outDir: 'dist', + assetsDir: 'assets', + sourcemap: false, + rollupOptions: { + output: { + manualChunks: { + vendor: ['vue'], + pdfjs: ['pdfjs-dist'] + } + } + } + }, optimizeDeps: { include: ['pdfjs-dist'] }