feat: better logging
This commit is contained in:
15
CLAUDE.md
15
CLAUDE.md
@@ -106,19 +106,16 @@ Start production frontend server (local):
|
|||||||
pnpm --filter @sexy.pivoine.art/frontend start
|
pnpm --filter @sexy.pivoine.art/frontend start
|
||||||
```
|
```
|
||||||
|
|
||||||
Docker deployment (recommended for production):
|
Docker Compose deployment (recommended for production):
|
||||||
```bash
|
```bash
|
||||||
# Build Docker image
|
# Local development (with Postgres, Redis, Directus)
|
||||||
docker build -t sexy.pivoine.art:latest .
|
docker-compose up -d
|
||||||
|
|
||||||
# Run with docker-compose
|
# Production (with Traefik, external DB, Redis)
|
||||||
docker-compose -f docker-compose.production.yml up -d
|
docker-compose -f compose.production.yml --env-file .env.production up -d
|
||||||
|
|
||||||
# Or run directly
|
|
||||||
docker run -d -p 3000:3000 --env-file .env.production sexy.pivoine.art:latest
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See `DOCKER.md` for comprehensive Docker deployment guide.
|
See `COMPOSE.md` for Docker Compose guide and `DOCKER.md` for standalone Docker deployment.
|
||||||
|
|
||||||
## Architecture Notes
|
## Architecture Notes
|
||||||
|
|
||||||
|
|||||||
424
COMPOSE.md
Normal file
424
COMPOSE.md
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
# Docker Compose Guide
|
||||||
|
|
||||||
|
This guide explains the Docker Compose setup for sexy.pivoine.art with local development and production configurations.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The application uses a **multi-file compose setup** with two configurations:
|
||||||
|
|
||||||
|
1. **`compose.yml`** - Base configuration for local development
|
||||||
|
2. **`compose.production.yml`** - Production overrides with Traefik integration
|
||||||
|
|
||||||
|
### Service Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🌐 Traefik Reverse Proxy (Production Only) │
|
||||||
|
│ ├─ HTTPS Termination │
|
||||||
|
│ ├─ Automatic Let's Encrypt │
|
||||||
|
│ └─ Routes traffic to frontend & Directus API │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 💄 Frontend (SvelteKit) │
|
||||||
|
│ ├─ Port 3000 (internal) │
|
||||||
|
│ ├─ Serves on https://sexy.pivoine.art │
|
||||||
|
│ └─ Proxies /api to Directus │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 🎭 Directus CMS │
|
||||||
|
│ ├─ Port 8055 (internal) │
|
||||||
|
│ ├─ Serves on https://sexy.pivoine.art/api │
|
||||||
|
│ ├─ Custom bundle extensions mounted │
|
||||||
|
│ └─ Uploads volume │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 🗄️ PostgreSQL (Local) / External (Production) │
|
||||||
|
│ └─ Database for Directus │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 💾 Redis (Local) / External (Production) │
|
||||||
|
│ └─ Cache & session storage │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker 20.10+
|
||||||
|
- Docker Compose 2.0+
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. **Create environment file:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your local settings (defaults work fine)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start all services:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Access services:**
|
||||||
|
- Frontend: http://localhost:3000 (if enabled)
|
||||||
|
- Directus: http://localhost:8055
|
||||||
|
- Directus Admin: http://localhost:8055/admin
|
||||||
|
|
||||||
|
4. **View logs:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Stop services:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Services
|
||||||
|
|
||||||
|
#### PostgreSQL
|
||||||
|
- **Image:** `postgres:16-alpine`
|
||||||
|
- **Port:** 5432 (internal only)
|
||||||
|
- **Volume:** `postgres-data`
|
||||||
|
- **Database:** `sexy`
|
||||||
|
|
||||||
|
#### Redis
|
||||||
|
- **Image:** `redis:7-alpine`
|
||||||
|
- **Port:** 6379 (internal only)
|
||||||
|
- **Volume:** `redis-data`
|
||||||
|
- **Persistence:** AOF enabled
|
||||||
|
|
||||||
|
#### Directus
|
||||||
|
- **Image:** `directus/directus:11`
|
||||||
|
- **Port:** 8055 (exposed)
|
||||||
|
- **Volumes:**
|
||||||
|
- `directus-uploads` - File uploads
|
||||||
|
- `./packages/bundle/dist` - Custom extensions
|
||||||
|
- **Features:**
|
||||||
|
- Auto-reload extensions
|
||||||
|
- WebSockets enabled
|
||||||
|
- CORS enabled for localhost
|
||||||
|
|
||||||
|
### Local Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start infrastructure (Postgres, Redis, Directus)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Develop frontend locally with hot reload
|
||||||
|
cd packages/frontend
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# Build Directus bundle
|
||||||
|
pnpm --filter @sexy.pivoine.art/bundle build
|
||||||
|
|
||||||
|
# Restart Directus to load new bundle
|
||||||
|
docker-compose restart directus
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- External PostgreSQL database
|
||||||
|
- External Redis instance
|
||||||
|
- Traefik reverse proxy configured
|
||||||
|
- External network: `compose_network`
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
The production compose file now uses the `include` directive to automatically extend `compose.yml`, making deployment simpler.
|
||||||
|
|
||||||
|
1. **Create production environment file:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Edit `.env.production` with your values:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database (external)
|
||||||
|
CORE_DB_HOST=your-postgres-host
|
||||||
|
SEXY_DB_NAME=sexy_production
|
||||||
|
DB_USER=sexy
|
||||||
|
DB_PASSWORD=your-secure-password
|
||||||
|
|
||||||
|
# Redis (external)
|
||||||
|
CORE_REDIS_HOST=your-redis-host
|
||||||
|
|
||||||
|
# Directus
|
||||||
|
SEXY_DIRECTUS_SECRET=your-32-char-random-secret
|
||||||
|
ADMIN_PASSWORD=your-secure-admin-password
|
||||||
|
|
||||||
|
# Traefik
|
||||||
|
SEXY_TRAEFIK_HOST=sexy.pivoine.art
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
PUBLIC_API_URL=https://sexy.pivoine.art/api
|
||||||
|
PUBLIC_URL=https://sexy.pivoine.art
|
||||||
|
|
||||||
|
# Email (SMTP)
|
||||||
|
EMAIL_SMTP_HOST=smtp.your-provider.com
|
||||||
|
EMAIL_SMTP_USER=your-email@domain.com
|
||||||
|
EMAIL_SMTP_PASSWORD=your-smtp-password
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Deploy:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple deployment - compose.production.yml includes compose.yml automatically
|
||||||
|
docker-compose -f compose.production.yml --env-file .env.production up -d
|
||||||
|
|
||||||
|
# Or use the traditional multi-file approach (same result)
|
||||||
|
docker-compose -f compose.yml -f compose.production.yml --env-file .env.production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Services
|
||||||
|
|
||||||
|
#### Directus
|
||||||
|
- **Image:** `directus/directus:11` (configurable)
|
||||||
|
- **Network:** `compose_network` (external)
|
||||||
|
- **Volumes:**
|
||||||
|
- `/var/www/sexy.pivoine.art/uploads` - Persistent uploads
|
||||||
|
- `/var/www/sexy.pivoine.art/packages/bundle/dist` - Extensions
|
||||||
|
- **Traefik routing:**
|
||||||
|
- Domain: `sexy.pivoine.art/api`
|
||||||
|
- Strips `/api` prefix before forwarding
|
||||||
|
- HTTPS with auto-certificates
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- **Image:** `ghcr.io/valknarxxx/sexy:latest` (from GHCR)
|
||||||
|
- **Network:** `compose_network` (external)
|
||||||
|
- **Volume:** `/var/www/sexy.pivoine.art` - Application code
|
||||||
|
- **Traefik routing:**
|
||||||
|
- Domain: `sexy.pivoine.art`
|
||||||
|
- HTTPS with auto-certificates
|
||||||
|
|
||||||
|
### Traefik Integration
|
||||||
|
|
||||||
|
Both services are configured with Traefik labels for automatic routing:
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- HTTP → HTTPS redirect
|
||||||
|
- Routes `sexy.pivoine.art` to port 3000
|
||||||
|
- Gzip compression enabled
|
||||||
|
|
||||||
|
**Directus API:**
|
||||||
|
- HTTP → HTTPS redirect
|
||||||
|
- Routes `sexy.pivoine.art/api` to port 8055
|
||||||
|
- Strips `/api` prefix
|
||||||
|
- Gzip compression enabled
|
||||||
|
|
||||||
|
### Production Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy/update (simplified - uses include)
|
||||||
|
docker-compose -f compose.production.yml --env-file .env.production up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose -f compose.production.yml logs -f
|
||||||
|
|
||||||
|
# Restart specific service
|
||||||
|
docker-compose -f compose.production.yml restart frontend
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
docker-compose -f compose.production.yml down
|
||||||
|
|
||||||
|
# Update images
|
||||||
|
docker-compose -f compose.production.yml pull
|
||||||
|
docker-compose -f compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Local Development (`.env`)
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DB_DATABASE` | `sexy` | Database name |
|
||||||
|
| `DB_USER` | `sexy` | Database user |
|
||||||
|
| `DB_PASSWORD` | `sexy` | Database password |
|
||||||
|
| `DIRECTUS_SECRET` | - | Secret for Directus (min 32 chars) |
|
||||||
|
| `ADMIN_EMAIL` | `admin@sexy.pivoine.art` | Admin email |
|
||||||
|
| `ADMIN_PASSWORD` | `admin` | Admin password |
|
||||||
|
| `CORS_ORIGIN` | `http://localhost:3000` | CORS allowed origins |
|
||||||
|
|
||||||
|
See `.env.example` for full list.
|
||||||
|
|
||||||
|
### Production (`.env.production`)
|
||||||
|
|
||||||
|
| Variable | Description | Required |
|
||||||
|
|----------|-------------|----------|
|
||||||
|
| `CORE_DB_HOST` | External PostgreSQL host | ✅ |
|
||||||
|
| `SEXY_DB_NAME` | Database name | ✅ |
|
||||||
|
| `DB_PASSWORD` | Database password | ✅ |
|
||||||
|
| `CORE_REDIS_HOST` | External Redis host | ✅ |
|
||||||
|
| `SEXY_DIRECTUS_SECRET` | Directus secret key | ✅ |
|
||||||
|
| `SEXY_TRAEFIK_HOST` | Domain name | ✅ |
|
||||||
|
| `EMAIL_SMTP_HOST` | SMTP server | ✅ |
|
||||||
|
| `EMAIL_SMTP_PASSWORD` | SMTP password | ✅ |
|
||||||
|
| `SEXY_FRONTEND_PUBLIC_API_URL` | Frontend API URL | ✅ |
|
||||||
|
| `SEXY_FRONTEND_PUBLIC_URL` | Frontend public URL | ✅ |
|
||||||
|
|
||||||
|
See `.env.production.example` for full list.
|
||||||
|
|
||||||
|
**Note:** All frontend-specific variables are prefixed with `SEXY_FRONTEND_` for clarity.
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
- `postgres-data` - PostgreSQL database
|
||||||
|
- `redis-data` - Redis persistence
|
||||||
|
- `directus-uploads` - Uploaded files
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
- `/var/www/sexy.pivoine.art/uploads` - Directus uploads
|
||||||
|
- `/var/www/sexy.pivoine.art` - Application code (frontend)
|
||||||
|
|
||||||
|
## Networks
|
||||||
|
|
||||||
|
### Local: `sexy-network`
|
||||||
|
- Bridge network
|
||||||
|
- Internal communication only
|
||||||
|
- Directus exposed on 8055
|
||||||
|
|
||||||
|
### Production: `compose_network`
|
||||||
|
- External network (pre-existing)
|
||||||
|
- Connects to Traefik
|
||||||
|
- No exposed ports (Traefik handles routing)
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
All services include health checks:
|
||||||
|
|
||||||
|
**PostgreSQL:**
|
||||||
|
- Command: `pg_isready`
|
||||||
|
- Interval: 10s
|
||||||
|
|
||||||
|
**Redis:**
|
||||||
|
- Command: `redis-cli ping`
|
||||||
|
- Interval: 10s
|
||||||
|
|
||||||
|
**Directus:**
|
||||||
|
- Endpoint: `/server/health`
|
||||||
|
- Interval: 30s
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- HTTP GET: `localhost:3000`
|
||||||
|
- Interval: 30s
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
**Problem:** Directus won't start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs directus
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# 1. Database not ready - wait for postgres to be healthy
|
||||||
|
# 2. Wrong secret - check DIRECTUS_SECRET is at least 32 chars
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Can't connect to database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if postgres is running
|
||||||
|
docker-compose ps postgres
|
||||||
|
|
||||||
|
# Verify health
|
||||||
|
docker-compose exec postgres pg_isready -U sexy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Extensions not loading
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rebuild bundle
|
||||||
|
pnpm --filter @sexy.pivoine.art/bundle build
|
||||||
|
|
||||||
|
# Verify volume mount
|
||||||
|
docker-compose exec directus ls -la /directus/extensions/
|
||||||
|
|
||||||
|
# Restart Directus
|
||||||
|
docker-compose restart directus
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
**Problem:** Services not accessible via domain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Traefik labels
|
||||||
|
docker inspect sexy_frontend | grep traefik
|
||||||
|
|
||||||
|
# Verify compose_network exists
|
||||||
|
docker network ls | grep compose_network
|
||||||
|
|
||||||
|
# Check Traefik is running
|
||||||
|
docker ps | grep traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Can't connect to external database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test connection from Directus container
|
||||||
|
docker-compose exec directus sh
|
||||||
|
apk add postgresql-client
|
||||||
|
psql -h $CORE_DB_HOST -U $DB_USER -d $SEXY_DB_NAME
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Frontend can't reach Directus API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Directus is accessible
|
||||||
|
curl https://sexy.pivoine.art/api/server/health
|
||||||
|
|
||||||
|
# Verify CORS settings
|
||||||
|
# PUBLIC_API_URL should match the public Directus URL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Old Setup
|
||||||
|
|
||||||
|
If migrating from `docker-compose.production.yml`:
|
||||||
|
|
||||||
|
1. **Rename environment variables** according to `.env.production.example`
|
||||||
|
2. **Update command** to use both compose files
|
||||||
|
3. **Verify Traefik labels** match your setup
|
||||||
|
4. **Test** with `docker-compose config` to see merged configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate configuration
|
||||||
|
docker-compose -f compose.yml -f compose.production.yml --env-file .env.production config
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
docker-compose -f compose.yml -f compose.production.yml --env-file .env.production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
1. Use default credentials (they're fine for local)
|
||||||
|
2. Keep `EXTENSIONS_AUTO_RELOAD=true` for quick iteration
|
||||||
|
3. Run frontend via `pnpm dev` for hot reload
|
||||||
|
4. Restart Directus after bundle changes
|
||||||
|
|
||||||
|
### Production
|
||||||
|
1. Use strong passwords for database and admin
|
||||||
|
2. Set `EXTENSIONS_AUTO_RELOAD=false` for stability
|
||||||
|
3. Use GHCR images for frontend
|
||||||
|
4. Enable Gzip compression via Traefik
|
||||||
|
5. Monitor logs regularly
|
||||||
|
6. Keep backups of uploads and database
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [DOCKER.md](DOCKER.md) - Docker image documentation
|
||||||
|
- [QUICKSTART.md](QUICKSTART.md) - Quick start guide
|
||||||
|
- [CLAUDE.md](CLAUDE.md) - Development guide
|
||||||
@@ -137,7 +137,8 @@ sexy.pivoine.art/
|
|||||||
| Document | Purpose | Emoji |
|
| Document | Purpose | Emoji |
|
||||||
|----------|---------|-------|
|
|----------|---------|-------|
|
||||||
| [QUICKSTART.md](QUICKSTART.md) | Get wet... I mean, get started! | 💦 |
|
| [QUICKSTART.md](QUICKSTART.md) | Get wet... I mean, get started! | 💦 |
|
||||||
| [DOCKER.md](DOCKER.md) | Complete deployment manual | 🐳 |
|
| [COMPOSE.md](COMPOSE.md) | Docker Compose setup guide | 🐳 |
|
||||||
|
| [DOCKER.md](DOCKER.md) | Standalone Docker deployment | 🐋 |
|
||||||
| [CLAUDE.md](CLAUDE.md) | Architecture & development | 🤖 |
|
| [CLAUDE.md](CLAUDE.md) | Architecture & development | 🤖 |
|
||||||
| [.github/workflows/README.md](.github/workflows/README.md) | CI/CD workflows | ⚙️ |
|
| [.github/workflows/README.md](.github/workflows/README.md) | CI/CD workflows | ⚙️ |
|
||||||
|
|
||||||
@@ -179,15 +180,15 @@ docker buildx build --platform linux/amd64,linux/arm64 -t sexy.pivoine.art:lates
|
|||||||
|
|
||||||
## 🚀 Deployment — Share Your Creation
|
## 🚀 Deployment — Share Your Creation
|
||||||
|
|
||||||
### Production with Docker
|
### Production with Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Configure your secrets
|
# Configure your secrets
|
||||||
cp .env.production.example .env.production
|
cp .env.production.example .env.production
|
||||||
# Edit .env.production with your intimate details
|
# Edit .env.production with your intimate details
|
||||||
|
|
||||||
# Deploy with grace
|
# Deploy with grace (uses Traefik for routing)
|
||||||
docker-compose -f docker-compose.production.yml up -d
|
docker-compose -f compose.production.yml --env-file .env.production up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production without Docker
|
### Production without Docker
|
||||||
|
|||||||
265
REBUILD_GUIDE.md
Normal file
265
REBUILD_GUIDE.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# 🔄 Rebuild Guide - When You Need to Rebuild the Image
|
||||||
|
|
||||||
|
## Why Rebuild?
|
||||||
|
|
||||||
|
SvelteKit's `PUBLIC_*` environment variables are **baked into the JavaScript** at build time. You need to rebuild when:
|
||||||
|
|
||||||
|
1. ✅ Changing `PUBLIC_API_URL`
|
||||||
|
2. ✅ Changing `PUBLIC_URL`
|
||||||
|
3. ✅ Changing `PUBLIC_UMAMI_ID`
|
||||||
|
4. ✅ Changing any `LETTERSPACE_*` variables
|
||||||
|
5. ❌ NOT needed for Directus env vars (those are runtime)
|
||||||
|
|
||||||
|
## Quick Rebuild Process
|
||||||
|
|
||||||
|
### 1. Update Frontend Environment Variables
|
||||||
|
|
||||||
|
Edit the frontend `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano packages/frontend/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Set your production values:
|
||||||
|
```bash
|
||||||
|
PUBLIC_API_URL=https://sexy.pivoine.art/api
|
||||||
|
PUBLIC_URL=https://sexy.pivoine.art
|
||||||
|
PUBLIC_UMAMI_ID=your-umami-id
|
||||||
|
LETTERSPACE_API_URL=https://api.letterspace.com/v1
|
||||||
|
LETTERSPACE_API_KEY=your-key
|
||||||
|
LETTERSPACE_LIST_ID=your-list-id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Rebuild the Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the project root
|
||||||
|
docker build -t ghcr.io/valknarxxx/sexy:latest -t sexy.pivoine.art:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Time:** 30-45 minutes (first build), 10-15 minutes (cached rebuild)
|
||||||
|
|
||||||
|
### 3. Restart Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If using docker-compose
|
||||||
|
cd /home/valknar/Projects/docker-compose/sexy
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Or directly
|
||||||
|
docker stop sexy_frontend
|
||||||
|
docker rm sexy_frontend
|
||||||
|
docker compose up -d frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring the Build
|
||||||
|
|
||||||
|
### Check Build Progress
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch build output
|
||||||
|
docker build -t ghcr.io/valknarxxx/sexy:latest .
|
||||||
|
|
||||||
|
# Build stages:
|
||||||
|
# 1. Base (~30s) - Node.js setup
|
||||||
|
# 2. Builder (~25-40min) - Rust + WASM + packages
|
||||||
|
# - Rust installation: ~2-3 min
|
||||||
|
# - wasm-bindgen-cli: ~10-15 min
|
||||||
|
# - WASM build: ~5-10 min
|
||||||
|
# - Package builds: ~5-10 min
|
||||||
|
# 3. Runner (~2min) - Final image assembly
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Environment Variables in Built Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what PUBLIC_API_URL is baked in
|
||||||
|
docker run --rm ghcr.io/valknarxxx/sexy:latest sh -c \
|
||||||
|
"grep -r 'PUBLIC_API_URL' /home/node/app/packages/frontend/build/ | head -3"
|
||||||
|
|
||||||
|
# Should show: https://sexy.pivoine.art/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Push to GitHub Container Registry
|
||||||
|
|
||||||
|
After successful build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login to GHCR (first time only)
|
||||||
|
echo $GITHUB_TOKEN | docker login ghcr.io -u valknarxxx --password-stdin
|
||||||
|
|
||||||
|
# Push the image
|
||||||
|
docker push ghcr.io/valknarxxx/sexy:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative: Build Arguments (Future Enhancement)
|
||||||
|
|
||||||
|
To avoid rebuilding for every env change, consider adding build arguments:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# In Dockerfile, before building frontend:
|
||||||
|
ARG PUBLIC_API_URL=https://sexy.pivoine.art/api
|
||||||
|
ARG PUBLIC_URL=https://sexy.pivoine.art
|
||||||
|
ARG PUBLIC_UMAMI_ID=
|
||||||
|
|
||||||
|
# Create .env.production dynamically
|
||||||
|
RUN echo "PUBLIC_API_URL=${PUBLIC_API_URL}" > packages/frontend/.env.production && \
|
||||||
|
echo "PUBLIC_URL=${PUBLIC_URL}" >> packages/frontend/.env.production && \
|
||||||
|
echo "PUBLIC_UMAMI_ID=${PUBLIC_UMAMI_ID}" >> packages/frontend/.env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
Then build with:
|
||||||
|
```bash
|
||||||
|
docker build \
|
||||||
|
--build-arg PUBLIC_API_URL=https://sexy.pivoine.art/api \
|
||||||
|
--build-arg PUBLIC_URL=https://sexy.pivoine.art \
|
||||||
|
-t ghcr.io/valknarxxx/sexy:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Fails at Rust Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check network connectivity
|
||||||
|
ping -c 3 sh.rustup.rs
|
||||||
|
|
||||||
|
# Build with verbose output
|
||||||
|
docker build --progress=plain -t ghcr.io/valknarxxx/sexy:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Fails at WASM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if wasm-bindgen-cli matches package.json version
|
||||||
|
docker run --rm rust:latest cargo install wasm-bindgen-cli --version 0.2.103
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Still Shows Wrong URL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify .env file is correct
|
||||||
|
cat packages/frontend/.env
|
||||||
|
|
||||||
|
# Check if old image is cached
|
||||||
|
docker images | grep sexy
|
||||||
|
docker rmi ghcr.io/valknarxxx/sexy:old-tag
|
||||||
|
|
||||||
|
# Force rebuild without cache
|
||||||
|
docker build --no-cache -t ghcr.io/valknarxxx/sexy:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Starts But Can't Connect to API
|
||||||
|
|
||||||
|
1. Check Traefik routing:
|
||||||
|
```bash
|
||||||
|
docker logs traefik | grep sexy
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Check if Directus is accessible:
|
||||||
|
```bash
|
||||||
|
curl -I https://sexy.pivoine.art/api/server/health
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Check frontend logs:
|
||||||
|
```bash
|
||||||
|
docker logs sexy_frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development vs Production
|
||||||
|
|
||||||
|
### Development (Local)
|
||||||
|
- Use `pnpm dev` for hot reload
|
||||||
|
- No rebuild needed for code changes
|
||||||
|
- Env vars from `.env` or shell
|
||||||
|
|
||||||
|
### Production (Docker)
|
||||||
|
- Rebuild required for PUBLIC_* changes
|
||||||
|
- Changes baked into JavaScript
|
||||||
|
- Env vars from `packages/frontend/.env`
|
||||||
|
|
||||||
|
## Optimization Tips
|
||||||
|
|
||||||
|
### Speed Up Rebuilds
|
||||||
|
|
||||||
|
1. **Use BuildKit cache:**
|
||||||
|
```bash
|
||||||
|
export DOCKER_BUILDKIT=1
|
||||||
|
docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t ghcr.io/valknarxxx/sexy:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Multi-stage caching:**
|
||||||
|
- Dockerfile already optimized with multi-stage build
|
||||||
|
- Dependencies cached separately from code
|
||||||
|
|
||||||
|
3. **Parallel builds:**
|
||||||
|
```bash
|
||||||
|
# Build with more CPU cores
|
||||||
|
docker build --cpus 4 -t ghcr.io/valknarxxx/sexy:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reduce Image Size
|
||||||
|
|
||||||
|
Current optimizations:
|
||||||
|
- ✅ Multi-stage build
|
||||||
|
- ✅ Production dependencies only
|
||||||
|
- ✅ Minimal base image
|
||||||
|
- ✅ No dev tools in final image
|
||||||
|
|
||||||
|
Expected sizes:
|
||||||
|
- Base: ~100MB
|
||||||
|
- Builder: ~2-3GB (not shipped)
|
||||||
|
- Runner: ~300-500MB (final)
|
||||||
|
|
||||||
|
## Automation
|
||||||
|
|
||||||
|
### GitHub Actions (Already Set Up)
|
||||||
|
|
||||||
|
The `.github/workflows/docker-build-push.yml` automatically:
|
||||||
|
1. Builds on push to main
|
||||||
|
2. Creates version tags
|
||||||
|
3. Pushes to GHCR
|
||||||
|
4. Caches layers for faster builds
|
||||||
|
|
||||||
|
**Trigger a rebuild:**
|
||||||
|
```bash
|
||||||
|
git tag v1.0.1
|
||||||
|
git push origin v1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Build Script
|
||||||
|
|
||||||
|
Use the provided `build.sh`:
|
||||||
|
```bash
|
||||||
|
./build.sh -t v1.0.0 -p
|
||||||
|
```
|
||||||
|
|
||||||
|
## When NOT to Rebuild
|
||||||
|
|
||||||
|
You DON'T need to rebuild for:
|
||||||
|
- ❌ Directus configuration changes
|
||||||
|
- ❌ Database credentials
|
||||||
|
- ❌ Redis settings
|
||||||
|
- ❌ SMTP settings
|
||||||
|
- ❌ Session cookie settings
|
||||||
|
- ❌ Traefik labels
|
||||||
|
|
||||||
|
These are runtime environment variables and can be changed in docker-compose.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Change | Rebuild Needed | How to Apply |
|
||||||
|
|--------|----------------|--------------|
|
||||||
|
| `PUBLIC_API_URL` | ✅ Yes | Rebuild image |
|
||||||
|
| `PUBLIC_URL` | ✅ Yes | Rebuild image |
|
||||||
|
| `PUBLIC_UMAMI_ID` | ✅ Yes | Rebuild image |
|
||||||
|
| `LETTERSPACE_*` | ✅ Yes | Rebuild image |
|
||||||
|
| `SEXY_DIRECTUS_*` | ❌ No | Restart container |
|
||||||
|
| `DB_*` | ❌ No | Restart container |
|
||||||
|
| `EMAIL_*` | ❌ No | Restart container |
|
||||||
|
| Traefik labels | ❌ No | Restart container |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember:** The key difference is **build-time** (compiled into JS) vs **runtime** (read from environment).
|
||||||
130
compose.production.yml
Normal file
130
compose.production.yml
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
include:
|
||||||
|
- compose.yml
|
||||||
|
|
||||||
|
# Production compose file - extends base compose.yml
|
||||||
|
# Usage: docker-compose -f compose.production.yml up -d
|
||||||
|
|
||||||
|
networks:
|
||||||
|
compose_network:
|
||||||
|
external: true
|
||||||
|
name: compose_network
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Disable local postgres for production (use external DB)
|
||||||
|
postgres:
|
||||||
|
deploy:
|
||||||
|
replicas: 0
|
||||||
|
|
||||||
|
# Disable local redis for production (use external Redis)
|
||||||
|
redis:
|
||||||
|
deploy:
|
||||||
|
replicas: 0
|
||||||
|
|
||||||
|
# Override Directus for production
|
||||||
|
directus:
|
||||||
|
networks:
|
||||||
|
- compose_network
|
||||||
|
ports: [] # Remove exposed ports, use Traefik instead
|
||||||
|
|
||||||
|
# Override volumes for production paths
|
||||||
|
volumes:
|
||||||
|
- ${SEXY_DIRECTUS_UPLOADS:-./uploads}:/directus/uploads
|
||||||
|
- ${SEXY_DIRECTUS_BUNDLE:-./packages/bundle/dist}:/directus/extensions/sexy.pivoine.art
|
||||||
|
|
||||||
|
# Override environment for production settings
|
||||||
|
environment:
|
||||||
|
# Database (external)
|
||||||
|
DB_HOST: ${CORE_DB_HOST}
|
||||||
|
DB_PORT: ${CORE_DB_PORT:-5432}
|
||||||
|
DB_DATABASE: ${SEXY_DB_NAME}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
|
||||||
|
# General
|
||||||
|
SECRET: ${SEXY_DIRECTUS_SECRET}
|
||||||
|
ADMIN_EMAIL: ${ADMIN_EMAIL}
|
||||||
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
||||||
|
PUBLIC_URL: ${SEXY_PUBLIC_URL}
|
||||||
|
|
||||||
|
# Cache (external Redis)
|
||||||
|
REDIS: redis://${CORE_REDIS_HOST}:${CORE_REDIS_PORT:-6379}
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN: ${SEXY_CORS_ORIGIN}
|
||||||
|
|
||||||
|
# Security (production settings)
|
||||||
|
SESSION_COOKIE_SECURE: ${SEXY_SESSION_COOKIE_SECURE:-true}
|
||||||
|
SESSION_COOKIE_SAME_SITE: ${SEXY_SESSION_COOKIE_SAME_SITE:-strict}
|
||||||
|
SESSION_COOKIE_DOMAIN: ${SEXY_SESSION_COOKIE_DOMAIN}
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
EXTENSIONS_AUTO_RELOAD: ${SEXY_EXTENSIONS_AUTO_RELOAD:-false}
|
||||||
|
|
||||||
|
# Email (production SMTP)
|
||||||
|
EMAIL_TRANSPORT: ${EMAIL_TRANSPORT:-smtp}
|
||||||
|
EMAIL_FROM: ${EMAIL_FROM}
|
||||||
|
EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST}
|
||||||
|
EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-587}
|
||||||
|
EMAIL_SMTP_USER: ${EMAIL_SMTP_USER}
|
||||||
|
EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD}
|
||||||
|
|
||||||
|
# User URLs
|
||||||
|
USER_REGISTER_URL_ALLOW_LIST: ${SEXY_USER_REGISTER_URL_ALLOW_LIST}
|
||||||
|
PASSWORD_RESET_URL_ALLOW_LIST: ${SEXY_PASSWORD_RESET_URL_ALLOW_LIST}
|
||||||
|
|
||||||
|
# Remove local dependencies
|
||||||
|
depends_on: []
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# Traefik labels for reverse proxy
|
||||||
|
- 'traefik.enable=${SEXY_TRAEFIK_ENABLED:-true}'
|
||||||
|
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-redirect-web-secure.redirectscheme.scheme=https'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-redirect-web-secure'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web.rule=Host(`${SEXY_TRAEFIK_HOST}`) && PathPrefix(`/api`)'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web.entrypoints=web'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.rule=Host(`${SEXY_TRAEFIK_HOST}`) && PathPrefix(`/api`)'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.tls.certresolver=resolver'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.entrypoints=web-secure'
|
||||||
|
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure-compress.compress=true'
|
||||||
|
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-strip.stripprefix.prefixes=/api'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-strip,${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure-compress'
|
||||||
|
- 'traefik.http.services.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.loadbalancer.server.port=8055'
|
||||||
|
- 'traefik.docker.network=compose_network'
|
||||||
|
|
||||||
|
# Override Frontend for production
|
||||||
|
frontend:
|
||||||
|
networks:
|
||||||
|
- compose_network
|
||||||
|
ports: [] # Remove exposed ports, use Traefik instead
|
||||||
|
|
||||||
|
# Override environment for production
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PUBLIC_API_URL: ${SEXY_FRONTEND_PUBLIC_API_URL}
|
||||||
|
PUBLIC_URL: ${SEXY_FRONTEND_PUBLIC_URL}
|
||||||
|
PUBLIC_UMAMI_ID: ${SEXY_FRONTEND_PUBLIC_UMAMI_ID:-}
|
||||||
|
LETTERSPACE_API_URL: ${SEXY_FRONTEND_LETTERSPACE_API_URL:-}
|
||||||
|
LETTERSPACE_API_KEY: ${SEXY_FRONTEND_LETTERSPACE_API_KEY:-}
|
||||||
|
LETTERSPACE_LIST_ID: ${SEXY_FRONTEND_LETTERSPACE_LIST_ID:-}
|
||||||
|
|
||||||
|
# Override volume for production path
|
||||||
|
volumes:
|
||||||
|
- ${SEXY_FRONTEND_PATH:-/var/www/sexy.pivoine.art}:/home/node/app
|
||||||
|
|
||||||
|
# Remove local dependency
|
||||||
|
depends_on: []
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# Traefik labels for reverse proxy
|
||||||
|
- 'traefik.enable=${SEXY_TRAEFIK_ENABLED:-true}'
|
||||||
|
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-redirect-web-secure.redirectscheme.scheme=https'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-redirect-web-secure'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web.rule=Host(`${SEXY_TRAEFIK_HOST}`)'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web.entrypoints=web'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.rule=Host(`${SEXY_TRAEFIK_HOST}`)'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.tls.certresolver=resolver'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.entrypoints=web-secure'
|
||||||
|
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure-compress.compress=true'
|
||||||
|
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure-compress'
|
||||||
|
- 'traefik.http.services.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.loadbalancer.server.port=3000'
|
||||||
|
- 'traefik.docker.network=compose_network'
|
||||||
183
compose.yml
Normal file
183
compose.yml
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
services:
|
||||||
|
# PostgreSQL Database (local only)
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- sexy-network
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${DB_DATABASE:-sexy}
|
||||||
|
POSTGRES_USER: ${DB_USER:-sexy}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-sexy}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-sexy}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Redis Cache (local only)
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- sexy-network
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Directus CMS
|
||||||
|
directus:
|
||||||
|
image: ${SEXY_DIRECTUS_IMAGE:-directus/directus:11}
|
||||||
|
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_api
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- sexy-network
|
||||||
|
ports:
|
||||||
|
- "8055:8055"
|
||||||
|
volumes:
|
||||||
|
- directus-uploads:/directus/uploads
|
||||||
|
- ${SEXY_DIRECTUS_BUNDLE:-./packages/bundle/dist}:/directus/extensions/sexy.pivoine.art
|
||||||
|
environment:
|
||||||
|
# Database
|
||||||
|
DB_CLIENT: pg
|
||||||
|
DB_HOST: ${CORE_DB_HOST:-postgres}
|
||||||
|
DB_PORT: ${CORE_DB_PORT:-5432}
|
||||||
|
DB_DATABASE: ${SEXY_DB_NAME:-sexy}
|
||||||
|
DB_USER: ${DB_USER:-sexy}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-sexy}
|
||||||
|
|
||||||
|
# General
|
||||||
|
SECRET: ${SEXY_DIRECTUS_SECRET:-replace-with-random-secret-min-32-chars}
|
||||||
|
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@sexy.pivoine.art}
|
||||||
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin}
|
||||||
|
PUBLIC_URL: ${SEXY_PUBLIC_URL:-http://localhost:8055}
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
CACHE_ENABLED: ${SEXY_CACHE_ENABLED:-true}
|
||||||
|
CACHE_AUTO_PURGE: ${SEXY_CACHE_AUTO_PURGE:-true}
|
||||||
|
CACHE_STORE: redis
|
||||||
|
REDIS: redis://${CORE_REDIS_HOST:-redis}:${CORE_REDIS_PORT:-6379}
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ENABLED: ${SEXY_CORS_ENABLED:-true}
|
||||||
|
CORS_ORIGIN: ${SEXY_CORS_ORIGIN:-http://localhost:3000}
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SESSION_COOKIE_SECURE: ${SEXY_SESSION_COOKIE_SECURE:-false}
|
||||||
|
SESSION_COOKIE_SAME_SITE: ${SEXY_SESSION_COOKIE_SAME_SITE:-lax}
|
||||||
|
SESSION_COOKIE_DOMAIN: ${SEXY_SESSION_COOKIE_DOMAIN:-localhost}
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
EXTENSIONS_PATH: ${SEXY_EXTENSIONS_PATH:-/directus/extensions}
|
||||||
|
EXTENSIONS_AUTO_RELOAD: ${SEXY_EXTENSIONS_AUTO_RELOAD:-true}
|
||||||
|
|
||||||
|
# WebSockets
|
||||||
|
WEBSOCKETS_ENABLED: ${SEXY_WEBSOCKETS_ENABLED:-true}
|
||||||
|
|
||||||
|
# Email (optional for local dev)
|
||||||
|
EMAIL_TRANSPORT: ${EMAIL_TRANSPORT:-sendmail}
|
||||||
|
EMAIL_FROM: ${EMAIL_FROM:-noreply@sexy.pivoine.art}
|
||||||
|
EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-}
|
||||||
|
EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-587}
|
||||||
|
EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||||
|
EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||||
|
|
||||||
|
# User Registration & Password Reset URLs
|
||||||
|
USER_REGISTER_URL_ALLOW_LIST: ${SEXY_USER_REGISTER_URL_ALLOW_LIST:-http://localhost:3000}
|
||||||
|
PASSWORD_RESET_URL_ALLOW_LIST: ${SEXY_PASSWORD_RESET_URL_ALLOW_LIST:-http://localhost:3000}
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC: ${SEXY_CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC:-}
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
TZ: ${TIMEZONE:-Europe/Amsterdam}
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8055/server/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Frontend (local development - optional, usually run via pnpm dev)
|
||||||
|
frontend:
|
||||||
|
image: ${SEXY_FRONTEND_IMAGE:-ghcr.io/valknarxxx/sexy:latest}
|
||||||
|
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
user: node
|
||||||
|
working_dir: /home/node/app/packages/frontend
|
||||||
|
networks:
|
||||||
|
- sexy-network
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
# Node
|
||||||
|
NODE_ENV: ${NODE_ENV:-development}
|
||||||
|
PORT: 3000
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
|
||||||
|
# Public environment variables
|
||||||
|
PUBLIC_API_URL: ${SEXY_FRONTEND_PUBLIC_API_URL:-http://localhost:8055}
|
||||||
|
PUBLIC_URL: ${SEXY_FRONTEND_PUBLIC_URL:-http://localhost:3000}
|
||||||
|
PUBLIC_UMAMI_ID: ${SEXY_FRONTEND_PUBLIC_UMAMI_ID:-}
|
||||||
|
|
||||||
|
# Letterspace newsletter integration
|
||||||
|
LETTERSPACE_API_URL: ${SEXY_FRONTEND_LETTERSPACE_API_URL:-}
|
||||||
|
LETTERSPACE_API_KEY: ${SEXY_FRONTEND_LETTERSPACE_API_KEY:-}
|
||||||
|
LETTERSPACE_LIST_ID: ${SEXY_FRONTEND_LETTERSPACE_LIST_ID:-}
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
TZ: ${TIMEZONE:-Europe/Amsterdam}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ${SEXY_FRONTEND_PATH:-./}:/home/node/app
|
||||||
|
|
||||||
|
command: ["node", "build/index.js"]
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- directus
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Uncomment to run frontend in development mode with live reload
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# volumes:
|
||||||
|
# - ./packages/frontend:/home/node/app/packages/frontend
|
||||||
|
# - /home/node/app/packages/frontend/node_modules
|
||||||
|
# environment:
|
||||||
|
# NODE_ENV: development
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sexy-network:
|
||||||
|
driver: bridge
|
||||||
|
name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
directus-uploads:
|
||||||
|
driver: local
|
||||||
|
postgres-data:
|
||||||
|
driver: local
|
||||||
|
redis-data:
|
||||||
|
driver: local
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
frontend:
|
|
||||||
# Use pre-built image from GitHub Container Registry
|
|
||||||
# To build locally instead, uncomment the 'build' section and comment out 'image'
|
|
||||||
image: ghcr.io/valknarxxx/sexy:latest
|
|
||||||
|
|
||||||
# Uncomment to build locally:
|
|
||||||
# build:
|
|
||||||
# context: .
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# args:
|
|
||||||
# NODE_ENV: production
|
|
||||||
container_name: sexy-pivoine-frontend
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
# Application settings
|
|
||||||
NODE_ENV: production
|
|
||||||
PORT: 3000
|
|
||||||
HOST: 0.0.0.0
|
|
||||||
|
|
||||||
# Public environment variables
|
|
||||||
PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:8055}
|
|
||||||
PUBLIC_URL: ${PUBLIC_URL:-http://localhost:3000}
|
|
||||||
PUBLIC_UMAMI_ID: ${PUBLIC_UMAMI_ID:-}
|
|
||||||
|
|
||||||
# Letterspace integration
|
|
||||||
LETTERSPACE_API_URL: ${LETTERSPACE_API_URL:-}
|
|
||||||
LETTERSPACE_API_KEY: ${LETTERSPACE_API_KEY:-}
|
|
||||||
LETTERSPACE_LIST_ID: ${LETTERSPACE_LIST_ID:-}
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- sexy-network
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
# Resource limits
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2'
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: '0.5'
|
|
||||||
memory: 512M
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
sexy-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
# Example usage:
|
|
||||||
# 1. Create a .env file with your environment variables
|
|
||||||
# 2. Build: docker-compose -f docker-compose.production.yml build
|
|
||||||
# 3. Run: docker-compose -f docker-compose.production.yml up -d
|
|
||||||
# 4. Logs: docker-compose -f docker-compose.production.yml logs -f
|
|
||||||
# 5. Stop: docker-compose -f docker-compose.production.yml down
|
|
||||||
1
packages/frontend/src/app.d.ts
vendored
1
packages/frontend/src/app.d.ts
vendored
@@ -8,6 +8,7 @@ declare global {
|
|||||||
// interface Error {}
|
// interface Error {}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
authStatus: AuthStatus;
|
authStatus: AuthStatus;
|
||||||
|
requestId: string;
|
||||||
}
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
|
|||||||
@@ -1,27 +1,97 @@
|
|||||||
import { isAuthenticated } from "$lib/services";
|
import { isAuthenticated } from "$lib/services";
|
||||||
|
import { logger, generateRequestId } from "$lib/logger";
|
||||||
|
import type { Handle } from "@sveltejs/kit";
|
||||||
|
|
||||||
export async function handle({ event, resolve }) {
|
// Log startup info once
|
||||||
const { cookies, locals } = event;
|
let hasLoggedStartup = false;
|
||||||
|
if (!hasLoggedStartup) {
|
||||||
|
logger.startup();
|
||||||
|
hasLoggedStartup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
const { cookies, locals, url, request } = event;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Generate unique request ID
|
||||||
|
const requestId = generateRequestId();
|
||||||
|
|
||||||
|
// Add request ID to locals for access in other handlers
|
||||||
|
locals.requestId = requestId;
|
||||||
|
|
||||||
|
// Log incoming request
|
||||||
|
logger.request(request.method, url.pathname, {
|
||||||
|
requestId,
|
||||||
|
context: {
|
||||||
|
userAgent: request.headers.get('user-agent')?.substring(0, 100),
|
||||||
|
referer: request.headers.get('referer'),
|
||||||
|
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle authentication
|
||||||
const token = cookies.get("directus_session_token");
|
const token = cookies.get("directus_session_token");
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
|
try {
|
||||||
locals.authStatus = await isAuthenticated(token);
|
locals.authStatus = await isAuthenticated(token);
|
||||||
// if (locals.authStatus.authenticated) {
|
|
||||||
// cookies.set('directus_refresh_token', locals.authStatus.data!.refresh_token!, {
|
if (locals.authStatus.authenticated) {
|
||||||
// httpOnly: true,
|
logger.auth('Token validated', true, {
|
||||||
// secure: true,
|
requestId,
|
||||||
// domain: '.pivoine.art',
|
userId: locals.authStatus.user?.id,
|
||||||
// path: '/'
|
context: {
|
||||||
// })
|
email: locals.authStatus.user?.email,
|
||||||
// }
|
role: locals.authStatus.user?.role?.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
|
logger.auth('Token invalid', false, { requestId });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Authentication check failed', {
|
||||||
|
requestId,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
});
|
||||||
|
locals.authStatus = { authenticated: false };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug('No session token found', { requestId });
|
||||||
locals.authStatus = { authenticated: false };
|
locals.authStatus = { authenticated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await resolve(event, {
|
// Resolve the request
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await resolve(event, {
|
||||||
filterSerializedResponseHeaders: (key) => {
|
filterSerializedResponseHeaders: (key) => {
|
||||||
return key.toLowerCase() === "content-type";
|
return key.toLowerCase() === "content-type";
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error('Request handler error', {
|
||||||
|
requestId,
|
||||||
|
method: request.method,
|
||||||
|
path: url.pathname,
|
||||||
|
duration,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log response
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.response(request.method, url.pathname, response.status, duration, {
|
||||||
|
requestId,
|
||||||
|
userId: locals.authStatus.authenticated ? locals.authStatus.user?.id : undefined,
|
||||||
|
context: {
|
||||||
|
cached: response.headers.get('x-sveltekit-page') === 'true',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add request ID to response headers (useful for debugging)
|
||||||
|
response.headers.set('x-request-id', requestId);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|||||||
148
packages/frontend/src/lib/logger.ts
Normal file
148
packages/frontend/src/lib/logger.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Server-side logging utility for sexy.pivoine.art
|
||||||
|
* Provides structured logging with context and request tracing
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
interface LogContext {
|
||||||
|
timestamp: string;
|
||||||
|
level: LogLevel;
|
||||||
|
message: string;
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
requestId?: string;
|
||||||
|
userId?: string;
|
||||||
|
path?: string;
|
||||||
|
method?: string;
|
||||||
|
duration?: number;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private isDev = process.env.NODE_ENV === 'development';
|
||||||
|
private serviceName = 'sexy.pivoine.art';
|
||||||
|
|
||||||
|
private formatLog(ctx: LogContext): string {
|
||||||
|
const { timestamp, level, message, context, requestId, userId, path, method, duration, error } = ctx;
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
`[${timestamp}]`,
|
||||||
|
`[${level.toUpperCase()}]`,
|
||||||
|
requestId ? `[${requestId}]` : null,
|
||||||
|
method && path ? `${method} ${path}` : null,
|
||||||
|
message,
|
||||||
|
userId ? `user=${userId}` : null,
|
||||||
|
duration !== undefined ? `${duration}ms` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
let logString = parts.join(' ');
|
||||||
|
|
||||||
|
if (context && Object.keys(context).length > 0) {
|
||||||
|
logString += ' ' + JSON.stringify(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logString += `\n Error: ${error.message}\n Stack: ${error.stack}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return logString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(level: LogLevel, message: string, meta: Partial<LogContext> = {}) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logContext: LogContext = {
|
||||||
|
timestamp,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
...meta,
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedLog = this.formatLog(logContext);
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
if (this.isDev) console.debug(formattedLog);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
console.info(formattedLog);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
console.warn(formattedLog);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.error(formattedLog);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string, meta?: Partial<LogContext>) {
|
||||||
|
this.log('debug', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, meta?: Partial<LogContext>) {
|
||||||
|
this.log('info', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, meta?: Partial<LogContext>) {
|
||||||
|
this.log('warn', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, meta?: Partial<LogContext>) {
|
||||||
|
this.log('error', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request logging helper
|
||||||
|
request(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
meta: Partial<LogContext> = {}
|
||||||
|
) {
|
||||||
|
this.info('→ Request received', { method, path, ...meta });
|
||||||
|
}
|
||||||
|
|
||||||
|
response(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
status: number,
|
||||||
|
duration: number,
|
||||||
|
meta: Partial<LogContext> = {}
|
||||||
|
) {
|
||||||
|
const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';
|
||||||
|
this.log(level, `← Response ${status}`, { method, path, duration, ...meta });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication logging
|
||||||
|
auth(action: string, success: boolean, meta: Partial<LogContext> = {}) {
|
||||||
|
this.info(`🔐 Auth: ${action} ${success ? 'success' : 'failed'}`, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Startup logging
|
||||||
|
startup() {
|
||||||
|
const env = {
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
PUBLIC_API_URL: process.env.PUBLIC_API_URL,
|
||||||
|
PUBLIC_URL: process.env.PUBLIC_URL,
|
||||||
|
PUBLIC_UMAMI_ID: process.env.PUBLIC_UMAMI_ID ? '***set***' : 'not set',
|
||||||
|
LETTERSPACE_API_URL: process.env.LETTERSPACE_API_URL || 'not set',
|
||||||
|
PORT: process.env.PORT || '3000',
|
||||||
|
HOST: process.env.HOST || '0.0.0.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(60));
|
||||||
|
console.log('🍑 sexy.pivoine.art - Server Starting 💜');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('\n📋 Environment Configuration:');
|
||||||
|
Object.entries(env).forEach(([key, value]) => {
|
||||||
|
console.log(` ${key}: ${value}`);
|
||||||
|
});
|
||||||
|
console.log('\n' + '='.repeat(60) + '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const logger = new Logger();
|
||||||
|
|
||||||
|
// Generate request ID
|
||||||
|
export function generateRequestId(): string {
|
||||||
|
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
@@ -18,6 +18,32 @@ import {
|
|||||||
} from "@directus/sdk";
|
} from "@directus/sdk";
|
||||||
import type { Article, Model, Stats, User, Video } from "$lib/types";
|
import type { Article, Model, Stats, User, Video } from "$lib/types";
|
||||||
import { PUBLIC_URL } from "$env/static/public";
|
import { PUBLIC_URL } from "$env/static/public";
|
||||||
|
import { logger } from "$lib/logger";
|
||||||
|
|
||||||
|
// Helper to log API calls
|
||||||
|
async function loggedApiCall<T>(
|
||||||
|
operationName: string,
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
context?: Record<string, unknown>
|
||||||
|
): Promise<T> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug(`🔄 API: ${operationName}`, { context });
|
||||||
|
const result = await operation();
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info(`✅ API: ${operationName} succeeded`, { duration, context });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.error(`❌ API: ${operationName} failed`, {
|
||||||
|
duration,
|
||||||
|
context,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userFields = [
|
const userFields = [
|
||||||
"*",
|
"*",
|
||||||
@@ -29,6 +55,9 @@ const userFields = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export async function isAuthenticated(token: string) {
|
export async function isAuthenticated(token: string) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"isAuthenticated",
|
||||||
|
async () => {
|
||||||
try {
|
try {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
directus.setToken(token);
|
directus.setToken(token);
|
||||||
@@ -41,6 +70,9 @@ export async function isAuthenticated(token: string) {
|
|||||||
} catch {
|
} catch {
|
||||||
return { authenticated: false };
|
return { authenticated: false };
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ hasToken: !!token },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function register(
|
export async function register(
|
||||||
@@ -49,6 +81,9 @@ export async function register(
|
|||||||
firstName: string,
|
firstName: string,
|
||||||
lastName: string,
|
lastName: string,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"register",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(
|
return directus.request(
|
||||||
registerUser(email, password, {
|
registerUser(email, password, {
|
||||||
@@ -57,38 +92,68 @@ export async function register(
|
|||||||
last_name: lastName,
|
last_name: lastName,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
{ email, firstName, lastName },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verify(token: string, fetch?: typeof globalThis.fetch) {
|
export async function verify(token: string, fetch?: typeof globalThis.fetch) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"verify",
|
||||||
|
async () => {
|
||||||
const directus = fetch
|
const directus = fetch
|
||||||
? getDirectusInstance((args) => fetch(args, { redirect: "manual" }))
|
? getDirectusInstance((args) => fetch(args, { redirect: "manual" }))
|
||||||
: getDirectusInstance(fetch);
|
: getDirectusInstance(fetch);
|
||||||
return directus.request(registerUserVerify(token));
|
return directus.request(registerUserVerify(token));
|
||||||
|
},
|
||||||
|
{ hasToken: !!token },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(email: string, password: string) {
|
export async function login(email: string, password: string) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"login",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.login({ email, password });
|
return directus.login({ email, password });
|
||||||
|
},
|
||||||
|
{ email },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
|
return loggedApiCall("logout", async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.logout();
|
return directus.logout();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestPassword(email: string) {
|
export async function requestPassword(email: string) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"requestPassword",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(
|
return directus.request(
|
||||||
passwordRequest(email, `${PUBLIC_URL || "http://localhost:3000"}/password/reset`),
|
passwordRequest(email, `${PUBLIC_URL || "http://localhost:3000"}/password/reset`),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
{ email },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetPassword(token: string, password: string) {
|
export async function resetPassword(token: string, password: string) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"resetPassword",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(passwordReset(token, password));
|
return directus.request(passwordReset(token, password));
|
||||||
|
},
|
||||||
|
{ hasToken: !!token },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getArticles(fetch?: typeof globalThis.fetch) {
|
export async function getArticles(fetch?: typeof globalThis.fetch) {
|
||||||
|
return loggedApiCall("getArticles", async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request<Article[]>(
|
return directus.request<Article[]>(
|
||||||
readItems("sexy_articles", {
|
readItems("sexy_articles", {
|
||||||
@@ -97,12 +162,16 @@ export async function getArticles(fetch?: typeof globalThis.fetch) {
|
|||||||
sort: ["-publish_date"],
|
sort: ["-publish_date"],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getArticleBySlug(
|
export async function getArticleBySlug(
|
||||||
slug: string,
|
slug: string,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getArticleBySlug",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus
|
return directus
|
||||||
.request<Article[]>(
|
.request<Article[]>(
|
||||||
@@ -117,9 +186,13 @@ export async function getArticleBySlug(
|
|||||||
}
|
}
|
||||||
return articles[0];
|
return articles[0];
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{ slug },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVideos(fetch?: typeof globalThis.fetch) {
|
export async function getVideos(fetch?: typeof globalThis.fetch) {
|
||||||
|
return loggedApiCall("getVideos", async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus
|
return directus
|
||||||
.request<Video[]>(
|
.request<Video[]>(
|
||||||
@@ -146,9 +219,13 @@ export async function getVideos(fetch?: typeof globalThis.fetch) {
|
|||||||
});
|
});
|
||||||
return videos;
|
return videos;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVideosForModel(id, fetch?: typeof globalThis.fetch) {
|
export async function getVideosForModel(id, fetch?: typeof globalThis.fetch) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getVideosForModel",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request<Video[]>(
|
return directus.request<Video[]>(
|
||||||
readItems("sexy_videos", {
|
readItems("sexy_videos", {
|
||||||
@@ -163,12 +240,18 @@ export async function getVideosForModel(id, fetch?: typeof globalThis.fetch) {
|
|||||||
sort: ["-upload_date"],
|
sort: ["-upload_date"],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
{ modelId: id },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeaturedVideos(
|
export async function getFeaturedVideos(
|
||||||
limit: number,
|
limit: number,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getFeaturedVideos",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus
|
return directus
|
||||||
.request<Video[]>(
|
.request<Video[]>(
|
||||||
@@ -199,12 +282,18 @@ export async function getFeaturedVideos(
|
|||||||
});
|
});
|
||||||
return videos;
|
return videos;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{ limit },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVideoBySlug(
|
export async function getVideoBySlug(
|
||||||
slug: string,
|
slug: string,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getVideoBySlug",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus
|
return directus
|
||||||
.request<Video[]>(
|
.request<Video[]>(
|
||||||
@@ -232,6 +321,9 @@ export async function getVideoBySlug(
|
|||||||
|
|
||||||
return videos[0];
|
return videos[0];
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{ slug },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelFilter = {
|
const modelFilter = {
|
||||||
@@ -256,6 +348,7 @@ const modelFilter = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getModels(fetch?: typeof globalThis.fetch) {
|
export async function getModels(fetch?: typeof globalThis.fetch) {
|
||||||
|
return loggedApiCall("getModels", async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request<Model[]>(
|
return directus.request<Model[]>(
|
||||||
readUsers({
|
readUsers({
|
||||||
@@ -264,12 +357,16 @@ export async function getModels(fetch?: typeof globalThis.fetch) {
|
|||||||
sort: ["-join_date"],
|
sort: ["-join_date"],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeaturedModels(
|
export async function getFeaturedModels(
|
||||||
limit = 3,
|
limit = 3,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getFeaturedModels",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request<Model[]>(
|
return directus.request<Model[]>(
|
||||||
readUsers({
|
readUsers({
|
||||||
@@ -279,12 +376,18 @@ export async function getFeaturedModels(
|
|||||||
limit,
|
limit,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
{ limit },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getModelBySlug(
|
export async function getModelBySlug(
|
||||||
slug: string,
|
slug: string,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getModelBySlug",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus
|
return directus
|
||||||
.request<Model[]>(
|
.request<Model[]>(
|
||||||
@@ -311,38 +414,62 @@ export async function getModelBySlug(
|
|||||||
models[0].photos = models[0].photos.map((p) => p.directus_files_id!);
|
models[0].photos = models[0].photos.map((p) => p.directus_files_id!);
|
||||||
return models[0];
|
return models[0];
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{ slug },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProfile(user: Partial<User>) {
|
export async function updateProfile(user: Partial<User>) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"updateProfile",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request<User>(updateMe(user as never));
|
return directus.request<User>(updateMe(user as never));
|
||||||
|
},
|
||||||
|
{ userId: user.id },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStats(fetch?: typeof globalThis.fetch) {
|
export async function getStats(fetch?: typeof globalThis.fetch) {
|
||||||
|
return loggedApiCall("getStats", async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request<Stats>(
|
return directus.request<Stats>(
|
||||||
customEndpoint({
|
customEndpoint({
|
||||||
path: "/sexy/stats",
|
path: "/sexy/stats",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFolders(fetch?: typeof globalThis.fetch) {
|
export async function getFolders(fetch?: typeof globalThis.fetch) {
|
||||||
|
return loggedApiCall("getFolders", async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(readFolders());
|
return directus.request(readFolders());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFile(id: string) {
|
export async function removeFile(id: string) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"removeFile",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(deleteFile(id));
|
return directus.request(deleteFile(id));
|
||||||
|
},
|
||||||
|
{ fileId: id },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadFile(data: FormData) {
|
export async function uploadFile(data: FormData) {
|
||||||
|
return loggedApiCall("uploadFile", async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(uploadFiles(data));
|
return directus.request(uploadFiles(data));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCommentForVideo(item: string, comment: string) {
|
export async function createCommentForVideo(item: string, comment: string) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"createCommentForVideo",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(
|
return directus.request(
|
||||||
createComment({
|
createComment({
|
||||||
@@ -351,12 +478,18 @@ export async function createCommentForVideo(item: string, comment: string) {
|
|||||||
comment,
|
comment,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
{ videoId: item, commentLength: comment.length },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCommentsForVideo(
|
export async function getCommentsForVideo(
|
||||||
item: string,
|
item: string,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getCommentsForVideo",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus.request(
|
return directus.request(
|
||||||
readComments({
|
readComments({
|
||||||
@@ -365,12 +498,18 @@ export async function getCommentsForVideo(
|
|||||||
sort: ["-date_created"],
|
sort: ["-date_created"],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
{ videoId: item },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countCommentsForModel(
|
export async function countCommentsForModel(
|
||||||
user_created: string,
|
user_created: string,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"countCommentsForModel",
|
||||||
|
async () => {
|
||||||
const directus = getDirectusInstance(fetch);
|
const directus = getDirectusInstance(fetch);
|
||||||
return directus
|
return directus
|
||||||
.request<[{ count: number }]>(
|
.request<[{ count: number }]>(
|
||||||
@@ -384,6 +523,9 @@ export async function countCommentsForModel(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then((result) => result[0].count);
|
.then((result) => result[0].count);
|
||||||
|
},
|
||||||
|
{ userId: user_created },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getItemsByTag(
|
export async function getItemsByTag(
|
||||||
@@ -391,6 +533,9 @@ export async function getItemsByTag(
|
|||||||
tag: string,
|
tag: string,
|
||||||
fetch?: typeof globalThis.fetch,
|
fetch?: typeof globalThis.fetch,
|
||||||
) {
|
) {
|
||||||
|
return loggedApiCall(
|
||||||
|
"getItemsByTag",
|
||||||
|
async () => {
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case "video":
|
case "video":
|
||||||
return getVideos(fetch);
|
return getVideos(fetch);
|
||||||
@@ -399,4 +544,7 @@ export async function getItemsByTag(
|
|||||||
case "article":
|
case "article":
|
||||||
return getArticles(fetch);
|
return getArticles(fetch);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ category, tag },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user