Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e634b89b70 | |||
| b39ca0930a | |||
| 822254f95e | |||
| c316db9b32 | |||
| d53fe4bb3e | |||
| 1c1792e7e1 | |||
| 0566b58240 | |||
| 88c153eb6a | |||
| 8c27b5ea2f |
@@ -1,297 +0,0 @@
|
||||
# SerpentRace Backend Build System
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the comprehensive build system for the SerpentRace backend application. The build system handles TypeScript compilation, database migrations, asset management, testing, and deployment.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Development build
|
||||
npm run build
|
||||
|
||||
# Production build with full validation
|
||||
npm run build:production
|
||||
|
||||
# Advanced build with migrations and tests
|
||||
npm run build:advanced:prod
|
||||
|
||||
# Development server with hot reload
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Build Scripts
|
||||
|
||||
### Basic Build Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run build` | Standard build: clean → compile → copy assets |
|
||||
| `npm run build:clean` | Clean the dist directory |
|
||||
| `npm run build:compile` | Compile TypeScript to JavaScript |
|
||||
| `npm run build:copy-assets` | Copy non-TS files to dist directory |
|
||||
| `npm run build:docker` | Build for Docker (no tests/migrations) |
|
||||
|
||||
### Production Build Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run build:production` | Full production build with linting, tests, and migrations |
|
||||
| `npm run build:advanced` | Advanced build script with custom options |
|
||||
| `npm run build:advanced:prod` | Advanced production build with all validations |
|
||||
| `npm run build:advanced:ci` | CI/CD friendly build (skips linting) |
|
||||
|
||||
### Development Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run dev` | Start development server with hot reload |
|
||||
| `npm run watch` | Watch mode TypeScript compilation |
|
||||
| `npm run typecheck` | Type checking without code generation |
|
||||
|
||||
### Database Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run migration:run` | Run pending database migrations |
|
||||
| `npm run migration:show` | Show migration status |
|
||||
| `npm run migration:generate <name>` | Generate new migration |
|
||||
| `npm run migration:create <name>` | Create empty migration |
|
||||
| `npm run migration:revert` | Revert last migration |
|
||||
| `npm run migration:full <name>` | Create, generate, and run migration |
|
||||
|
||||
### Testing Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm test` | Run all tests |
|
||||
| `npm run test:watch` | Run tests in watch mode |
|
||||
| `npm run test:coverage` | Run tests with coverage report |
|
||||
| `npm run test:redis` | Run Redis-specific tests |
|
||||
|
||||
### Deployment Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run deploy:prod` | Build for production deployment |
|
||||
| `scripts/deploy.sh` | Full Linux/Mac deployment script |
|
||||
| `scripts/deploy.bat` | Full Windows deployment script |
|
||||
|
||||
## Advanced Build Script
|
||||
|
||||
The advanced build script (`scripts/build.ts`) supports various options:
|
||||
|
||||
```bash
|
||||
# Basic advanced build
|
||||
npm run build:advanced
|
||||
|
||||
# Production build with migrations and tests
|
||||
npm run build:advanced:prod
|
||||
|
||||
# CI/CD build (skips linting, includes tests and migrations)
|
||||
npm run build:advanced:ci
|
||||
```
|
||||
|
||||
### Build Options
|
||||
|
||||
- `--migrations`: Run database migrations during build
|
||||
- `--test`: Run tests during build
|
||||
- `--skip-lint`: Skip linting step
|
||||
- `--production`: Enable production mode (strict validation)
|
||||
|
||||
## Deployment Scripts
|
||||
|
||||
### Linux/Mac Deployment
|
||||
|
||||
```bash
|
||||
./scripts/deploy.sh [deploy|build-only|test-connections]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `deploy` (default): Full deployment with validation
|
||||
- `build-only`: Build without connection testing
|
||||
- `test-connections`: Test database and Redis connections only
|
||||
|
||||
### Windows Deployment
|
||||
|
||||
```cmd
|
||||
scripts\deploy.bat [deploy|build-only|test-connections]
|
||||
```
|
||||
|
||||
Same options as Linux/Mac version.
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
The deployment scripts require these environment variables:
|
||||
|
||||
```bash
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=your_password
|
||||
DB_NAME=serpentrace
|
||||
JWT_SECRET=your_jwt_secret
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
|
||||
## Build Process Flow
|
||||
|
||||
### Standard Build (`npm run build`)
|
||||
|
||||
1. **Clean** - Remove previous build artifacts
|
||||
2. **Lint** - Code quality checks (if configured)
|
||||
3. **Compile** - TypeScript compilation
|
||||
4. **Copy Assets** - Copy non-TS files to dist
|
||||
5. **Post-build** - Validation and cleanup
|
||||
|
||||
### Production Build (`npm run build:production`)
|
||||
|
||||
1. **Clean** - Remove previous build artifacts
|
||||
2. **Lint** - Code quality checks
|
||||
3. **Test** - Run test suite
|
||||
4. **Migrations** - Apply database migrations
|
||||
5. **Compile** - TypeScript compilation
|
||||
6. **Copy Assets** - Copy non-TS files to dist
|
||||
7. **Validate** - Ensure build integrity
|
||||
|
||||
### Advanced Build (`npm run build:advanced`)
|
||||
|
||||
Provides fine-grained control over the build process with comprehensive logging and error handling.
|
||||
|
||||
## Asset Management
|
||||
|
||||
The build system automatically copies these file types to the dist directory:
|
||||
|
||||
- `.json` files (configuration, data)
|
||||
- `.html` files (templates)
|
||||
- `.css` files (stylesheets)
|
||||
- Image files (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`)
|
||||
- Font files (`.woff`, `.woff2`, `.ttf`, `.eot`)
|
||||
|
||||
Excluded directories:
|
||||
- `node_modules`
|
||||
- `.git`
|
||||
- `tests`
|
||||
- `__tests__`
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
The build system uses the following TypeScript settings:
|
||||
|
||||
- **Target**: ES2020
|
||||
- **Module**: CommonJS
|
||||
- **Output Directory**: `./dist`
|
||||
- **Source Maps**: Enabled
|
||||
- **Declarations**: Enabled for type definitions
|
||||
- **Strict Mode**: Enabled for type safety
|
||||
|
||||
## Migration Management
|
||||
|
||||
### Creating Migrations
|
||||
|
||||
```bash
|
||||
# Create empty migration
|
||||
npm run migration:create AddNewTable
|
||||
|
||||
# Generate migration from entity changes
|
||||
npm run migration:generate AddNewTable
|
||||
|
||||
# Full migration workflow (create + generate + run)
|
||||
npm run migration:full AddNewTable
|
||||
```
|
||||
|
||||
### Migration Best Practices
|
||||
|
||||
1. Always backup database before running migrations in production
|
||||
2. Test migrations in development environment first
|
||||
3. Use descriptive migration names
|
||||
4. Review generated migrations before running them
|
||||
|
||||
## Docker Integration
|
||||
|
||||
The build system is optimized for Docker deployments:
|
||||
|
||||
```dockerfile
|
||||
# Use build:docker for container builds
|
||||
RUN npm run build:docker
|
||||
|
||||
# Or use production build for full validation
|
||||
RUN npm run build:production
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build fails with "Cannot find module"**
|
||||
- Run `npm ci` to ensure all dependencies are installed
|
||||
- Check TypeScript paths configuration
|
||||
|
||||
2. **Migration errors during build**
|
||||
- Verify database connection parameters
|
||||
- Ensure database exists and is accessible
|
||||
- Check migration files for syntax errors
|
||||
|
||||
3. **Asset copying fails**
|
||||
- Verify file permissions
|
||||
- Check disk space availability
|
||||
- Ensure source files exist
|
||||
|
||||
4. **TypeScript compilation errors**
|
||||
- Run `npm run typecheck` for detailed error messages
|
||||
- Check tsconfig.json configuration
|
||||
- Verify all type definitions are installed
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable verbose logging by setting the environment variable:
|
||||
|
||||
```bash
|
||||
export DEBUG=serpentrace:*
|
||||
npm run build:advanced
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Build Performance Tips
|
||||
|
||||
1. Use `npm ci` instead of `npm install` in CI/CD
|
||||
2. Enable TypeScript incremental compilation for development
|
||||
3. Use `--skip-lint` in CI if linting is handled separately
|
||||
4. Cache node_modules in CI/CD pipelines
|
||||
|
||||
### Runtime Performance
|
||||
|
||||
The build system optimizes the output for production:
|
||||
|
||||
- Source maps for debugging (can be disabled in production)
|
||||
- Type declarations for library usage
|
||||
- Compressed and optimized JavaScript output
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
Build logs include:
|
||||
- Timestamps for each build step
|
||||
- Error details with stack traces
|
||||
- Performance metrics (build duration)
|
||||
- Validation results
|
||||
|
||||
Production builds create detailed logs in the `logs/` directory.
|
||||
|
||||
## Contributing
|
||||
|
||||
When modifying the build system:
|
||||
|
||||
1. Test changes with both development and production builds
|
||||
2. Update this documentation for any new scripts or options
|
||||
3. Ensure backward compatibility
|
||||
4. Add appropriate error handling and logging
|
||||
|
||||
## Support
|
||||
|
||||
For build system issues:
|
||||
1. Check this documentation
|
||||
2. Review error logs in the console
|
||||
3. Verify environment variables are set correctly
|
||||
4. Test with a clean `node_modules` installation
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,392 +0,0 @@
|
||||
# 🗄️ SerpentRace Database Management Guide
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This guide provides comprehensive information about managing all database services in the SerpentRace project, including PostgreSQL, Redis, MinIO, and administration tools.
|
||||
|
||||
## 📊 Quick Status Check
|
||||
|
||||
### Check All Services
|
||||
```bash
|
||||
npm run db:status
|
||||
```
|
||||
|
||||
### Check Individual Services
|
||||
```bash
|
||||
npm run db:status:pg # PostgreSQL only
|
||||
npm run db:status:redis # Redis only
|
||||
npm run db:status:docker # Docker containers only
|
||||
```
|
||||
|
||||
### Simple Connection Test
|
||||
```bash
|
||||
npm run test:connections
|
||||
```
|
||||
|
||||
## 🐘 PostgreSQL Database
|
||||
|
||||
### Connection Details
|
||||
- **Host**: localhost:5432
|
||||
- **Database**: serpentrace
|
||||
- **Username**: postgres
|
||||
- **Password**: postgres
|
||||
- **Admin Tool**: pgAdmin at http://localhost:8080
|
||||
|
||||
### Database Operations
|
||||
|
||||
#### Run Migrations
|
||||
```bash
|
||||
npm run migration:run
|
||||
```
|
||||
|
||||
#### Create New Migration
|
||||
```bash
|
||||
npm run migration:create src/migrations/YourMigrationName
|
||||
```
|
||||
|
||||
#### Generate Migration from Entity Changes
|
||||
```bash
|
||||
npm run migration:generate src/migrations/YourMigrationName
|
||||
```
|
||||
|
||||
#### Check Migration Status
|
||||
```bash
|
||||
npm run migration:show
|
||||
```
|
||||
|
||||
#### Rollback Last Migration
|
||||
```bash
|
||||
npm run migration:revert
|
||||
```
|
||||
|
||||
### Direct Database Access
|
||||
|
||||
#### Using psql (if installed)
|
||||
```bash
|
||||
psql -h localhost -p 5432 -U postgres -d serpentrace
|
||||
```
|
||||
|
||||
#### Using pgAdmin
|
||||
1. Open http://localhost:8080
|
||||
2. Login with: admin@serpentrace.dev / admin
|
||||
3. Server should be pre-configured as "SerpentRace"
|
||||
|
||||
### Common SQL Queries
|
||||
|
||||
#### Check Database Size
|
||||
```sql
|
||||
SELECT pg_size_pretty(pg_database_size('serpentrace')) as size;
|
||||
```
|
||||
|
||||
#### List All Tables
|
||||
```sql
|
||||
SELECT tablename FROM pg_tables WHERE schemaname = 'public';
|
||||
```
|
||||
|
||||
#### Check Active Connections
|
||||
```sql
|
||||
SELECT count(*) FROM pg_stat_activity WHERE datname = 'serpentrace';
|
||||
```
|
||||
|
||||
## 🔴 Redis Cache
|
||||
|
||||
### Connection Details
|
||||
- **Host**: localhost:6379
|
||||
- **No Authentication**: Default Redis setup
|
||||
- **Admin Tool**: Redis Commander at http://localhost:8081
|
||||
|
||||
### Redis Operations
|
||||
|
||||
#### Direct Redis Access (if redis-cli installed)
|
||||
```bash
|
||||
redis-cli -h localhost -p 6379
|
||||
```
|
||||
|
||||
#### Common Redis Commands
|
||||
```bash
|
||||
# Get all keys
|
||||
KEYS *
|
||||
|
||||
# Get key count
|
||||
DBSIZE
|
||||
|
||||
# Check memory usage
|
||||
INFO memory
|
||||
|
||||
# Flush all data (careful!)
|
||||
FLUSHALL
|
||||
```
|
||||
|
||||
### Using Redis Commander
|
||||
1. Open http://localhost:8081
|
||||
2. Browse keys, view data, execute commands
|
||||
|
||||
## 🗄️ MinIO Object Storage
|
||||
|
||||
### Connection Details
|
||||
- **Endpoint**: localhost:9000
|
||||
- **Console**: http://localhost:9001
|
||||
- **Access Key**: serpentrace
|
||||
- **Secret Key**: serpentrace123
|
||||
- **Default Bucket**: serpentrace
|
||||
|
||||
### MinIO Operations
|
||||
|
||||
#### Access MinIO Console
|
||||
1. Open http://localhost:9001
|
||||
2. Login with: serpentrace / serpentrace123
|
||||
3. Create buckets, upload files, manage storage
|
||||
|
||||
#### Health Check
|
||||
```bash
|
||||
curl http://localhost:9000/minio/health/live
|
||||
```
|
||||
|
||||
### File Upload Example (Node.js)
|
||||
```javascript
|
||||
const Minio = require('minio');
|
||||
|
||||
const minioClient = new Minio.Client({
|
||||
endPoint: 'localhost',
|
||||
port: 9000,
|
||||
useSSL: false,
|
||||
accessKey: 'serpentrace',
|
||||
secretKey: 'serpentrace123'
|
||||
});
|
||||
|
||||
// Upload file
|
||||
minioClient.fPutObject('serpentrace', 'test-file.txt', './file.txt');
|
||||
```
|
||||
|
||||
## 🐳 Docker Container Management
|
||||
|
||||
### View All Containers
|
||||
```bash
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
### View SerpentRace Containers Only
|
||||
```bash
|
||||
docker ps -a --filter "name=serpentrace"
|
||||
```
|
||||
|
||||
### Container Operations
|
||||
|
||||
#### Restart All Services
|
||||
```bash
|
||||
cd d:\munka\SzeSnake\SerpentRace_Docker
|
||||
docker-compose -f docker-compose.dev.yml restart
|
||||
```
|
||||
|
||||
#### Restart Individual Service
|
||||
```bash
|
||||
docker restart serpentrace-postgres-dev # PostgreSQL
|
||||
docker restart serpentrace-redis-dev # Redis
|
||||
docker restart serpentrace-minio-dev # MinIO
|
||||
docker restart serpentrace-pgadmin-dev # pgAdmin
|
||||
```
|
||||
|
||||
#### View Container Logs
|
||||
```bash
|
||||
docker logs serpentrace-postgres-dev
|
||||
docker logs serpentrace-redis-dev -f # Follow logs
|
||||
```
|
||||
|
||||
#### Stop All Services
|
||||
```bash
|
||||
cd d:\munka\SzeSnake\SerpentRace_Docker
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
```
|
||||
|
||||
#### Start All Services
|
||||
```bash
|
||||
cd d:\munka\SzeSnake\SerpentRace_Docker
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### PostgreSQL Issues
|
||||
|
||||
#### Connection Refused
|
||||
```bash
|
||||
# Check if container is running
|
||||
docker ps | grep postgres
|
||||
|
||||
# Check container logs
|
||||
docker logs serpentrace-postgres-dev
|
||||
|
||||
# Restart if needed
|
||||
docker restart serpentrace-postgres-dev
|
||||
```
|
||||
|
||||
#### Migration Errors
|
||||
```bash
|
||||
# Check migration status
|
||||
npm run migration:show
|
||||
|
||||
# Revert last migration if problematic
|
||||
npm run migration:revert
|
||||
|
||||
# Re-run migrations
|
||||
npm run migration:run
|
||||
```
|
||||
|
||||
### Redis Issues
|
||||
|
||||
#### Cannot Connect
|
||||
```bash
|
||||
# Check Redis container
|
||||
docker ps | grep redis
|
||||
|
||||
# Test connection
|
||||
redis-cli -h localhost -p 6379 ping
|
||||
# Expected response: PONG
|
||||
```
|
||||
|
||||
### MinIO Issues
|
||||
|
||||
#### Health Check Failed
|
||||
```bash
|
||||
# Check MinIO container
|
||||
docker ps | grep minio
|
||||
|
||||
# Test health endpoint
|
||||
curl http://localhost:9000/minio/health/live
|
||||
# Expected response: 200 OK
|
||||
```
|
||||
|
||||
### pgAdmin Issues
|
||||
|
||||
#### Cannot Login
|
||||
- Default credentials: admin@serpentrace.dev / admin
|
||||
- If issues persist, restart container:
|
||||
```bash
|
||||
docker restart serpentrace-pgladmin-dev
|
||||
```
|
||||
|
||||
#### Server Not Found
|
||||
- pgAdmin should auto-configure the PostgreSQL server
|
||||
- If not visible, add manually:
|
||||
- Host: postgres
|
||||
- Port: 5432
|
||||
- Database: serpentrace
|
||||
- Username: postgres
|
||||
- Password: postgres
|
||||
|
||||
## 🔧 Environment Variables
|
||||
|
||||
### Default Development Settings
|
||||
```bash
|
||||
# PostgreSQL
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=serpentrace
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
# MinIO
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=serpentrace
|
||||
MINIO_SECRET_KEY=serpentrace123
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
Create `.env.production` with secure values:
|
||||
```bash
|
||||
DB_HOST=your-production-host
|
||||
DB_PASSWORD=secure-password
|
||||
REDIS_PASSWORD=secure-redis-password
|
||||
MINIO_SECRET_KEY=secure-minio-secret
|
||||
```
|
||||
|
||||
## 📈 Monitoring & Maintenance
|
||||
|
||||
### Daily Health Check
|
||||
```bash
|
||||
npm run db:status
|
||||
```
|
||||
|
||||
### Weekly Maintenance
|
||||
```bash
|
||||
# Check database size growth
|
||||
npm run db:status:pg
|
||||
|
||||
# Review Redis memory usage
|
||||
npm run db:status:redis
|
||||
|
||||
# Clean up old Docker logs
|
||||
docker system prune
|
||||
```
|
||||
|
||||
### Backup Procedures
|
||||
|
||||
#### PostgreSQL Backup
|
||||
```bash
|
||||
docker exec serpentrace-postgres-dev pg_dump -U postgres serpentrace > backup.sql
|
||||
```
|
||||
|
||||
#### Redis Backup
|
||||
```bash
|
||||
docker exec serpentrace-redis-dev redis-cli BGSAVE
|
||||
```
|
||||
|
||||
#### MinIO Backup
|
||||
Use MinIO Console or mc client to backup buckets.
|
||||
|
||||
## 🎯 Performance Optimization
|
||||
|
||||
### PostgreSQL
|
||||
- Monitor active connections with `npm run db:status:pg`
|
||||
- Use connection pooling in production
|
||||
- Regular VACUUM and ANALYZE operations
|
||||
|
||||
### Redis
|
||||
- Monitor memory usage
|
||||
- Configure appropriate eviction policies
|
||||
- Use Redis persistence (RDB/AOF) in production
|
||||
|
||||
### MinIO
|
||||
- Configure appropriate bucket policies
|
||||
- Use lifecycle management for old files
|
||||
- Monitor storage usage through console
|
||||
|
||||
## 🚀 Quick Reference Commands
|
||||
|
||||
```bash
|
||||
# Status and Health
|
||||
npm run db:status # Full system status
|
||||
npm run test:connections # Quick connection test
|
||||
|
||||
# Database Operations
|
||||
npm run migration:run # Apply migrations
|
||||
npm run migration:show # Check migration status
|
||||
|
||||
# Docker Management
|
||||
docker ps # Show running containers
|
||||
docker logs <container> # View logs
|
||||
docker restart <container> # Restart service
|
||||
|
||||
# Direct Access
|
||||
psql -h localhost -U postgres -d serpentrace # PostgreSQL CLI
|
||||
redis-cli -h localhost # Redis CLI
|
||||
```
|
||||
|
||||
## 🌐 Web Interfaces Summary
|
||||
|
||||
| Service | URL | Credentials |
|
||||
|---------|-----|------------|
|
||||
| pgAdmin | http://localhost:8080 | admin@serpentrace.dev / admin |
|
||||
| Redis Commander | http://localhost:8081 | No auth required |
|
||||
| MinIO Console | http://localhost:9001 | serpentrace / serpentrace123 |
|
||||
| Backend API | http://localhost:3000 | When running |
|
||||
| Frontend | http://localhost:5173 | When running |
|
||||
|
||||
---
|
||||
|
||||
*This guide is automatically updated when database configurations change. Last updated: 2025-08-23*
|
||||
@@ -1,235 +0,0 @@
|
||||
# Docker Watcher Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains the Docker watcher implementation for the SerpentRace project, which automatically synchronizes local file changes with Docker containers and rebuilds images when necessary.
|
||||
|
||||
## What's Implemented
|
||||
|
||||
### Docker Compose Watch Configuration
|
||||
|
||||
The development Docker Compose configuration now includes `develop.watch` sections for both frontend and backend services that provide:
|
||||
|
||||
1. **File Synchronization**: Automatically sync source code changes to running containers
|
||||
2. **Selective Rebuilding**: Rebuild containers when critical configuration files change
|
||||
3. **Intelligent Ignore Patterns**: Exclude unnecessary files like `node_modules`
|
||||
|
||||
### Backend Watcher Configuration
|
||||
|
||||
```yaml
|
||||
develop:
|
||||
watch:
|
||||
- action: sync
|
||||
path: ../SerpentRace_Backend/src
|
||||
target: /app/src
|
||||
ignore:
|
||||
- node_modules/
|
||||
- action: sync
|
||||
path: ../SerpentRace_Backend/package.json
|
||||
target: /app/package.json
|
||||
- action: rebuild
|
||||
path: ../SerpentRace_Backend/package-lock.json
|
||||
- action: rebuild
|
||||
path: ../SerpentRace_Docker/Dockerfile_backend.dev
|
||||
```
|
||||
|
||||
### Frontend Watcher Configuration
|
||||
|
||||
```yaml
|
||||
develop:
|
||||
watch:
|
||||
- action: sync
|
||||
path: ../SerpentRace_Frontend/src
|
||||
target: /app/src
|
||||
ignore:
|
||||
- node_modules/
|
||||
- action: sync
|
||||
path: ../SerpentRace_Frontend/public
|
||||
target: /app/public
|
||||
- action: sync
|
||||
path: ../SerpentRace_Frontend/package.json
|
||||
target: /app/package.json
|
||||
- action: rebuild
|
||||
path: ../SerpentRace_Frontend/package-lock.json
|
||||
- action: rebuild
|
||||
path: ../SerpentRace_Frontend/vite.config.js
|
||||
- action: rebuild
|
||||
path: ../SerpentRace_Docker/Dockerfile_frontend.dev
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Sync Actions
|
||||
- **Purpose**: Instantly copy changed files from host to container
|
||||
- **Use Cases**: Source code files, static assets, configuration files that don't require rebuild
|
||||
- **Performance**: Near-instant updates, no container restart needed
|
||||
|
||||
### Rebuild Actions
|
||||
- **Purpose**: Trigger full container rebuild when critical files change
|
||||
- **Use Cases**: Package files, Docker configuration, build configuration
|
||||
- **Performance**: Takes longer but ensures consistency
|
||||
|
||||
## Usage
|
||||
|
||||
### New Commands Added
|
||||
|
||||
#### Windows (docker-manage.bat)
|
||||
```bash
|
||||
# Start with file watchers
|
||||
.\docker-manage.bat dev:watch
|
||||
|
||||
# Traditional start (without watchers)
|
||||
.\docker-manage.bat dev:start
|
||||
```
|
||||
|
||||
#### Linux/Mac (docker-manage.sh)
|
||||
```bash
|
||||
# Start with file watchers
|
||||
./docker-manage.sh dev:watch
|
||||
|
||||
# Traditional start (without watchers)
|
||||
./docker-manage.sh dev:start
|
||||
```
|
||||
|
||||
### Command Differences
|
||||
|
||||
| Command | Mode | File Watching | Container Rebuild | Use Case |
|
||||
|---------|------|---------------|-------------------|----------|
|
||||
| `dev:start` | Background (-d) | No | Manual only | Traditional development |
|
||||
| `dev:watch` | Foreground | Yes | Automatic | Modern development with live sync |
|
||||
|
||||
## Benefits
|
||||
|
||||
### 1. Instant File Synchronization
|
||||
- Source code changes are immediately available in containers
|
||||
- No manual rebuild or restart required for code changes
|
||||
- Maintains all existing hot-reload functionality (nodemon, Vite HMR)
|
||||
|
||||
### 2. Smart Rebuilding
|
||||
- Automatically rebuilds when package.json or Dockerfile changes
|
||||
- Ensures containers stay consistent with dependency updates
|
||||
- Prevents common issues with stale dependencies
|
||||
|
||||
### 3. Development Efficiency
|
||||
- Combines Docker's isolation with native-like development speed
|
||||
- Reduces context switching between local and containerized development
|
||||
- Maintains consistent environment across team members
|
||||
|
||||
## File Patterns Watched
|
||||
|
||||
### Backend
|
||||
- **Synced Files**:
|
||||
- `src/` directory (all TypeScript source files)
|
||||
- `package.json` (for runtime reference)
|
||||
- **Rebuild Triggers**:
|
||||
- `package-lock.json` (dependency changes)
|
||||
- `Dockerfile_backend.dev` (container configuration)
|
||||
|
||||
### Frontend
|
||||
- **Synced Files**:
|
||||
- `src/` directory (React components, styles, etc.)
|
||||
- `public/` directory (static assets)
|
||||
- `package.json` (for runtime reference)
|
||||
- **Rebuild Triggers**:
|
||||
- `package-lock.json` (dependency changes)
|
||||
- `vite.config.js` (build configuration)
|
||||
- `Dockerfile_frontend.dev` (container configuration)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Sync Performance
|
||||
- File synchronization is near-instantaneous
|
||||
- Uses Docker's built-in file watching mechanisms
|
||||
- Optimized for development workloads
|
||||
|
||||
### Rebuild Performance
|
||||
- Rebuilds only occur when necessary
|
||||
- Docker layer caching reduces rebuild times
|
||||
- Can be resource-intensive for large dependency changes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **File Changes Not Reflected**
|
||||
- Ensure you're using `dev:watch` command
|
||||
- Check that files are not in ignore patterns
|
||||
- Verify file paths are correct
|
||||
|
||||
2. **Excessive Rebuilds**
|
||||
- Check for unnecessary changes to rebuild trigger files
|
||||
- Consider moving files to sync-only patterns if appropriate
|
||||
|
||||
3. **Performance Issues**
|
||||
- Monitor Docker resource usage
|
||||
- Consider excluding large directories from watching
|
||||
- Use `.dockerignore` for files that should never be synced
|
||||
|
||||
### Debugging Commands
|
||||
|
||||
```bash
|
||||
# Check container status
|
||||
docker-compose -f SerpentRace_Docker/docker-compose.dev.yml ps
|
||||
|
||||
# View watcher logs
|
||||
docker-compose -f SerpentRace_Docker/docker-compose.dev.yml logs -f backend
|
||||
docker-compose -f SerpentRace_Docker/docker-compose.dev.yml logs -f frontend
|
||||
|
||||
# Check file synchronization
|
||||
docker exec -it serpentrace-backend-dev ls -la /app/src
|
||||
docker exec -it serpentrace-frontend-dev ls -la /app/src
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Docker Compose Version
|
||||
- Requires Docker Compose v2.22+ for `develop.watch` support
|
||||
- Check version: `docker-compose version`
|
||||
|
||||
### File System
|
||||
- Works on Windows, Linux, and macOS
|
||||
- Performance may vary based on file system type
|
||||
- WSL2 recommended for Windows users
|
||||
|
||||
## Migration from Traditional Setup
|
||||
|
||||
### No Breaking Changes
|
||||
- Existing `dev:start` command continues to work
|
||||
- All volume mounts remain functional
|
||||
- Hot reload functionality preserved
|
||||
|
||||
### Gradual Adoption
|
||||
1. Try `dev:watch` for active development
|
||||
2. Use `dev:start` for background services
|
||||
3. Gradually migrate team to new workflow
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Development Workflow
|
||||
1. Use `dev:watch` during active development
|
||||
2. Make code changes normally
|
||||
3. Watch for automatic synchronization
|
||||
4. Monitor logs for any sync issues
|
||||
|
||||
### File Organization
|
||||
- Keep frequently changed files in sync patterns
|
||||
- Place build configuration in rebuild patterns
|
||||
- Use `.dockerignore` for files that should never sync
|
||||
|
||||
### Team Collaboration
|
||||
- Document which command team members should use
|
||||
- Ensure consistent Docker Compose version across team
|
||||
- Share troubleshooting steps for common issues
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
1. **Selective Service Watching**: Watch only specific services
|
||||
2. **Custom Ignore Patterns**: Per-developer ignore configurations
|
||||
3. **Performance Monitoring**: Built-in sync performance metrics
|
||||
4. **Integration with IDEs**: Better editor integration for sync status
|
||||
|
||||
### Configuration Expansion
|
||||
- Additional file patterns as needed
|
||||
- Service-specific watch configurations
|
||||
- Environment-based watch rules
|
||||
@@ -1,570 +0,0 @@
|
||||
# Frontend Kódolási Útmutató - SerpentRace
|
||||
|
||||
## Tartalomjegyzék
|
||||
1. [Navigáció és Routing](#navigáció-és-routing)
|
||||
2. [Fájl és Mappa Struktúra](#fájl-és-mappa-struktúra)
|
||||
3. [Komponens Konvenciók](#komponens-konvenciók)
|
||||
4. [State Management](#state-management)
|
||||
5. [API Hívások](#api-hívások)
|
||||
6. [Hibakezelés](#hibakezelés)
|
||||
7. [Elnevezési Konvenciók](#elnevezési-konvenciók)
|
||||
|
||||
---
|
||||
|
||||
## Navigáció és Routing
|
||||
|
||||
### ✅ Helyes gyakorlat: HandleNavigate használata
|
||||
|
||||
**MINDIG használd a központosított HandleNavigate hook-ot navigációhoz:**
|
||||
|
||||
```jsx
|
||||
import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate"
|
||||
|
||||
const MyComponent = () => {
|
||||
const { goHome, goLogin, goDeckDetails } = HandleNavigate()
|
||||
|
||||
const handleClick = () => {
|
||||
goHome() // Egyszerű navigáció
|
||||
}
|
||||
|
||||
const handleDeckView = (deckId) => {
|
||||
goDeckDetails(deckId) // Dinamikus route paraméterrel
|
||||
}
|
||||
|
||||
const handleLobby = (gameCode) => {
|
||||
goLobby({ gameCode }) // State passzolással
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Kerülendő: Direkt useNavigate használat
|
||||
|
||||
```jsx
|
||||
// SOHA NE HASZNÁLD EZT!
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
const MyComponent = () => {
|
||||
const navigate = useNavigate()
|
||||
navigate("/home") // ❌ NEM JÓ!
|
||||
}
|
||||
```
|
||||
|
||||
### Elérhető Navigációs Függvények
|
||||
|
||||
**HandleNavigate által biztosított függvények:**
|
||||
|
||||
```jsx
|
||||
const {
|
||||
// Általános
|
||||
goTo, // goTo('/any-path', { state: {...} })
|
||||
goBack, // Vissza az előző oldalra
|
||||
|
||||
// Authentikáció
|
||||
goHome, // → /home
|
||||
goLogin, // → /login, state: { success, message }
|
||||
goRegister, // → /register (alias: goAuth)
|
||||
goLanding, // → / (landing page)
|
||||
|
||||
// Deck Management
|
||||
goDecks, // → /decks
|
||||
goDeckDetails, // goDeckDetails(deckId) → /deck/:deckId
|
||||
goDeckCreator, // → /deck-creator
|
||||
goDeckCreatorEdit, // goDeckCreatorEdit(deckId) → /deck-creator/:deckId
|
||||
|
||||
// Game Flow
|
||||
goLobby, // goLobby({ gameCode }) → /lobby
|
||||
goChooseDeck, // goChooseDeck({ username, deckIds }) → /choosedeck
|
||||
goPlayerSetup, // goPlayerSetup({ deckIds }) → /player-setup
|
||||
goGame, // goGame({ players, gameState }) → /game
|
||||
|
||||
// Egyéb
|
||||
goContacts // → /contacts (alias: goCompanies)
|
||||
} = HandleNavigate()
|
||||
```
|
||||
|
||||
### Route Konstansok
|
||||
|
||||
**Használd a centralizált route konstansokat:**
|
||||
|
||||
```jsx
|
||||
// src/utils/routes.js
|
||||
import { ROUTES } from '../../utils/routes'
|
||||
|
||||
// App.jsx-ben
|
||||
<Route path={ROUTES.HOME} element={<Home />} />
|
||||
<Route path={ROUTES.DECK_DETAILS} element={<DeckDetails />} />
|
||||
|
||||
// ❌ NE használj string literálokat:
|
||||
<Route path="/home" element={<Home />} /> // NEM JÓ!
|
||||
```
|
||||
|
||||
### State Passzolás
|
||||
|
||||
**Így adj át adatokat navigáció során:**
|
||||
|
||||
```jsx
|
||||
// Régi mód (useNavigate) - ❌ NE!
|
||||
navigate('/lobby', { state: { gameCode: 'ABC123' } })
|
||||
|
||||
// Új mód (HandleNavigate) - ✅ JÓ!
|
||||
goLobby({ gameCode: 'ABC123' })
|
||||
|
||||
// Fogadó oldalon:
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
const Lobby = () => {
|
||||
const location = useLocation()
|
||||
const gameCode = location.state?.gameCode
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fájl és Mappa Struktúra
|
||||
|
||||
### Mappa Szervezés
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # API hívások
|
||||
│ ├── userApi.js
|
||||
│ ├── deckApi.js
|
||||
│ └── gameApi.js
|
||||
├── assets/ # Statikus fájlok
|
||||
│ ├── backgrounds/
|
||||
│ ├── images/
|
||||
│ └── icons/
|
||||
├── components/ # Újrahasználható komponensek
|
||||
│ ├── Buttons/
|
||||
│ ├── Inputs/
|
||||
│ ├── Navbar/
|
||||
│ └── PopUp/
|
||||
├── hooks/ # Custom Hooks
|
||||
│ └── useRequireAuth.jsx
|
||||
├── pages/ # Oldal komponensek
|
||||
│ ├── Auth/
|
||||
│ ├── Game/
|
||||
│ ├── Decks/
|
||||
│ └── Landing/
|
||||
├── utils/ # Utility függvények
|
||||
│ ├── HandleNavigate/
|
||||
│ └── routes.js
|
||||
└── App.jsx # Fő alkalmazás komponens
|
||||
```
|
||||
|
||||
### Fájl Elnevezési Konvenciók
|
||||
|
||||
- **Komponensek**: PascalCase
|
||||
- `LoginForm.jsx`, `DeckCreator.jsx`, `ButtonGreen.jsx`
|
||||
|
||||
- **Utility fájlok**: camelCase
|
||||
- `routes.js`, `randomUtils.js`, `userApi.js`
|
||||
|
||||
- **Hook fájlok**: camelCase, "use" prefix
|
||||
- `useRequireAuth.jsx`, `useLocalStorage.jsx`
|
||||
|
||||
---
|
||||
|
||||
## Komponens Konvenciók
|
||||
|
||||
### Funkcionális Komponens Sablon
|
||||
|
||||
```jsx
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import HandleNavigate from '../../utils/HandleNavigate/HandleNavigate'
|
||||
|
||||
/**
|
||||
* Komponens rövid leírása
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const MyComponent = () => {
|
||||
// 1. Hooks (HandleNavigate, useState, useEffect, stb.)
|
||||
const { goHome } = HandleNavigate()
|
||||
const [data, setData] = useState(null)
|
||||
|
||||
// 2. Effect hooks
|
||||
useEffect(() => {
|
||||
// Component mount logic
|
||||
}, [])
|
||||
|
||||
// 3. Event handlers
|
||||
const handleClick = () => {
|
||||
// Logic
|
||||
}
|
||||
|
||||
// 4. Render
|
||||
return (
|
||||
<div>
|
||||
{/* JSX */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyComponent
|
||||
```
|
||||
|
||||
### Import Sorrend
|
||||
|
||||
```jsx
|
||||
// 1. React és third-party library-k
|
||||
import React, { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
// 2. React Router hooks (useLocation, useParams - NEM useNavigate!)
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
// 3. Custom hooks és utils
|
||||
import HandleNavigate from '../../utils/HandleNavigate/HandleNavigate'
|
||||
import useRequireAuth from '../../hooks/useRequireAuth'
|
||||
|
||||
// 4. API
|
||||
import { getUserData } from '../../api/userApi'
|
||||
|
||||
// 5. Komponensek
|
||||
import Button from '../../components/Buttons/Button'
|
||||
import Navbar from '../../components/Navbar/Navbar'
|
||||
|
||||
// 6. Assets
|
||||
import Background from '../../assets/backgrounds/Background'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### Local State
|
||||
|
||||
```jsx
|
||||
// Egyszerű state
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
// Object state
|
||||
const [user, setUser] = useState({
|
||||
name: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
// Array state
|
||||
const [items, setItems] = useState([])
|
||||
```
|
||||
|
||||
### LocalStorage Használat
|
||||
|
||||
**useRequireAuth hook használata auth kezeléshez:**
|
||||
|
||||
```jsx
|
||||
import useRequireAuth from '../../hooks/useRequireAuth'
|
||||
|
||||
const MyComponent = () => {
|
||||
const [username] = useRequireAuth({
|
||||
key: 'username',
|
||||
redirectTo: '/login'
|
||||
})
|
||||
|
||||
// username automatikusan szinkronizálva van localStorage-el
|
||||
// Ha nincs username, automatikus redirect /login-re
|
||||
}
|
||||
```
|
||||
|
||||
**Manuális localStorage:**
|
||||
|
||||
```jsx
|
||||
// Írás
|
||||
localStorage.setItem('gameToken', token)
|
||||
|
||||
// Olvasás
|
||||
const token = localStorage.getItem('gameToken')
|
||||
|
||||
// Törlés
|
||||
localStorage.removeItem('gameToken')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Hívások
|
||||
|
||||
### API File Struktúra
|
||||
|
||||
**Minden API endpoint egy külön file-ban (`userApi.js`, `deckApi.js`, `gameApi.js`):**
|
||||
|
||||
```jsx
|
||||
// src/api/userApi.js
|
||||
import axiosInstance from './axiosInstance'
|
||||
|
||||
export const getUserData = async (userId) => {
|
||||
try {
|
||||
const response = await axiosInstance.get(`/users/${userId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updateUser = async (userId, userData) => {
|
||||
try {
|
||||
const response = await axiosInstance.put(`/users/${userId}`, userData)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Hívás Komponensben
|
||||
|
||||
```jsx
|
||||
import { getUserData } from '../../api/userApi'
|
||||
|
||||
const MyComponent = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [data, setData] = useState(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await getUserData(userId)
|
||||
setData(result)
|
||||
} catch (err) {
|
||||
setError(err.message || 'Hiba történt')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [userId])
|
||||
|
||||
if (loading) return <div>Betöltés...</div>
|
||||
if (error) return <div>Hiba: {error}</div>
|
||||
|
||||
return <div>{/* data megjelenítése */}</div>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hibakezelés
|
||||
|
||||
### Try-Catch Blokkok
|
||||
|
||||
```jsx
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const response = await createDeck(deckData)
|
||||
// Siker kezelése
|
||||
notifySuccess('Deck sikeresen létrehozva!')
|
||||
goDecks()
|
||||
} catch (error) {
|
||||
// Hiba kezelése
|
||||
const errorMessage = error.response?.data?.message || 'Ismeretlen hiba'
|
||||
setError(errorMessage)
|
||||
notifyError(errorMessage)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Toast Notifications
|
||||
|
||||
```jsx
|
||||
import { notifySuccess, notifyError } from '../../components/Toastify/toastifyServices'
|
||||
|
||||
// Siker üzenet
|
||||
notifySuccess('✅ Művelet sikeres!')
|
||||
|
||||
// Hiba üzenet
|
||||
notifyError('❌ Hiba történt!')
|
||||
|
||||
// Egyedi konfiguráció
|
||||
notifySuccess('Mentve!', { autoClose: 2000 })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Elnevezési Konvenciók
|
||||
|
||||
### JavaScript/React
|
||||
|
||||
| Típus | Konvenció | Példa |
|
||||
|-------|-----------|-------|
|
||||
| Komponensek | PascalCase | `LoginForm`, `DeckCreator` |
|
||||
| Függvények | camelCase | `handleClick`, `fetchUserData` |
|
||||
| Változók | camelCase | `userName`, `isLoading` |
|
||||
| Konstansok | UPPER_SNAKE_CASE | `API_BASE_URL`, `MAX_PLAYERS` |
|
||||
| Private változók | _camelCase | `_internalState` |
|
||||
| Event handlers | handle + PascalCase | `handleSubmit`, `handleInputChange` |
|
||||
| Boolean változók | is/has/can prefix | `isLoading`, `hasError`, `canEdit` |
|
||||
|
||||
### CSS Classes (Tailwind)
|
||||
|
||||
```jsx
|
||||
// Használj explicit class neveket
|
||||
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow-md">
|
||||
|
||||
// Kerüld a túl hosszú class stringeket - bontsd több sorra
|
||||
<div
|
||||
className="
|
||||
flex items-center justify-between
|
||||
p-4 bg-white rounded-lg shadow-md
|
||||
hover:shadow-lg transition-shadow duration-200
|
||||
"
|
||||
>
|
||||
```
|
||||
|
||||
### Fájl Nevek
|
||||
|
||||
- **Egyedi komponens**: `LoginForm.jsx` (nem `login-form.jsx`)
|
||||
- **Index fájlok**: `index.jsx` (ha könyvtárban több file van)
|
||||
- **Utility fájlok**: `randomUtils.js` (camelCase)
|
||||
- **API fájlok**: `userApi.js` (camelCase + Api postfix)
|
||||
|
||||
---
|
||||
|
||||
## Teljes Példa - Best Practices
|
||||
|
||||
```jsx
|
||||
// src/pages/Example/ExamplePage.jsx
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
import HandleNavigate from '../../utils/HandleNavigate/HandleNavigate'
|
||||
import useRequireAuth from '../../hooks/useRequireAuth'
|
||||
import { fetchExampleData, updateExampleData } from '../../api/exampleApi'
|
||||
import { notifySuccess, notifyError } from '../../components/Toastify/toastifyServices'
|
||||
|
||||
import Navbar from '../../components/Navbar/Navbar'
|
||||
import Button from '../../components/Buttons/Button'
|
||||
import Background from '../../assets/backgrounds/Background'
|
||||
|
||||
/**
|
||||
* Example Page - Komponens rövid leírása
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const ExamplePage = () => {
|
||||
// 1. Auth & Navigation
|
||||
const [username] = useRequireAuth({ key: 'username', redirectTo: '/login' })
|
||||
const { goHome, goBack } = HandleNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// 2. State
|
||||
const [data, setData] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
// 3. Effects
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// 4. Functions
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await fetchExampleData()
|
||||
setData(result)
|
||||
} catch (err) {
|
||||
const errorMsg = err.response?.data?.message || 'Hiba történt'
|
||||
setError(errorMsg)
|
||||
notifyError(errorMsg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await updateExampleData(data)
|
||||
notifySuccess('✅ Sikeresen mentve!')
|
||||
goHome()
|
||||
} catch (err) {
|
||||
notifyError('❌ Mentés sikertelen')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
goBack()
|
||||
}
|
||||
|
||||
// 5. Conditional Rendering
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div>Betöltés...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-red-500">Hiba: {error}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 6. Main Render
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<Background />
|
||||
<Navbar />
|
||||
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h1 className="text-3xl font-bold mb-6">Example Page</h1>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
{data && (
|
||||
<div>
|
||||
{/* Render data */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-4 mt-6">
|
||||
<Button onClick={handleSave} text="Mentés" />
|
||||
<Button onClick={handleCancel} text="Mégse" variant="secondary" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExamplePage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Összefoglalás - Legfontosabb Szabályok
|
||||
|
||||
1. ✅ **MINDIG használd HandleNavigate-et** navigációhoz
|
||||
2. ✅ **Használd a ROUTES konstansokat** az App.jsx-ben
|
||||
3. ✅ **API hívások külön file-okban** (userApi.js, deckApi.js, stb.)
|
||||
4. ✅ **Try-catch minden async műveletnél**
|
||||
5. ✅ **Toast notifications** a felhasználói visszajelzéshez
|
||||
6. ✅ **useRequireAuth hook** auth védett oldalaknál
|
||||
7. ✅ **Konzisztens import sorrend**
|
||||
8. ✅ **PascalCase komponenseknek, camelCase változóknak**
|
||||
9. ❌ **SOHA ne használj useNavigate közvetlen**
|
||||
10. ❌ **Ne használj string literal route-okat**
|
||||
|
||||
---
|
||||
|
||||
**Verzió:** 1.0
|
||||
**Utolsó frissítés:** 2025-01-17
|
||||
**Készítette:** SerpentRace Development Team
|
||||
@@ -1,476 +0,0 @@
|
||||
# Frontend Game Completion Implementation
|
||||
|
||||
**Date:** November 19, 2025
|
||||
**Status:** ✅ Core gameplay event handlers and action methods implemented
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document details the completion of missing WebSocket event handlers and action methods in the frontend to enable full gameplay functionality. The implementation ensures that GameScreen can properly receive and respond to all game events from the backend.
|
||||
|
||||
---
|
||||
|
||||
## Changes Implemented
|
||||
|
||||
### 1. GameWebSocketContext.jsx - Added Missing Event Handlers
|
||||
|
||||
**Location:** `SerpentRace_Frontend/src/contexts/GameWebSocketContext.jsx`
|
||||
|
||||
Added 9 critical gameplay event handlers that were missing from the context:
|
||||
|
||||
#### ✅ game:your-turn
|
||||
- **Purpose:** Notifies player when it's their turn
|
||||
- **Action:** Updates currentTurn state, emits custom event for GameScreen
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:your-turn', (data) => {
|
||||
log('🎯 Your turn!', data);
|
||||
setCurrentTurn(data.currentPlayer);
|
||||
window.dispatchEvent(new CustomEvent('game:your-turn', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:dice-rolled
|
||||
- **Purpose:** Broadcasts dice roll results to all players
|
||||
- **Action:** Emits custom event for UI to display dice animation
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:dice-rolled', (data) => {
|
||||
log('🎲 Dice rolled:', data.diceValue, 'by', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:dice-rolled', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:guess-result
|
||||
- **Purpose:** Receives position guess validation result
|
||||
- **Action:** Updates player position if guess was correct, emits event
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:guess-result', (data) => {
|
||||
log('🎯 Guess result:', data);
|
||||
if (data.correct && data.newPosition !== undefined) {
|
||||
setBoardData(prev => {
|
||||
if (!prev) return prev;
|
||||
const updatedPlayers = { ...prev.playerPositions };
|
||||
updatedPlayers[data.playerName] = data.newPosition;
|
||||
return { ...prev, playerPositions: updatedPlayers };
|
||||
});
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('game:guess-result', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:joker-complete
|
||||
- **Purpose:** Receives joker card approval/rejection result
|
||||
- **Action:** Updates player position if joker was approved, emits event
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:joker-complete', (data) => {
|
||||
log('🃏 Joker complete:', data);
|
||||
if (data.approved && data.newPosition !== undefined) {
|
||||
setBoardData(prev => {
|
||||
if (!prev) return prev;
|
||||
const updatedPlayers = { ...prev.playerPositions };
|
||||
updatedPlayers[data.playerName] = data.newPosition;
|
||||
return { ...prev, playerPositions: updatedPlayers };
|
||||
});
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('game:joker-complete', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:luck-consequence
|
||||
- **Purpose:** Receives luck card consequence (extra turns, lost turns, position changes)
|
||||
- **Action:** Updates player position if consequence includes movement, emits event
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:luck-consequence', (data) => {
|
||||
log('🍀 Luck consequence:', data);
|
||||
if (data.newPosition !== undefined && data.playerName) {
|
||||
setBoardData(prev => {
|
||||
if (!prev) return prev;
|
||||
const updatedPlayers = { ...prev.playerPositions };
|
||||
updatedPlayers[data.playerName] = data.newPosition;
|
||||
return { ...prev, playerPositions: updatedPlayers };
|
||||
});
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('game:luck-consequence', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:ended
|
||||
- **Purpose:** Announces game end with winner and final scores
|
||||
- **Action:** Updates gameState with winner and final scores, emits event for winner modal
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:ended', (data) => {
|
||||
log('🏁 Game ended! Winner:', data.winner);
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
status: 'finished',
|
||||
winner: data.winner,
|
||||
finalScores: data.scores
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent('game:ended', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:extra-turn-remaining
|
||||
- **Purpose:** Notifies player they have extra turn(s) from luck consequences
|
||||
- **Action:** Emits event for UI notification
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:extra-turn-remaining', (data) => {
|
||||
log('⭐ Extra turn remaining:', data);
|
||||
window.dispatchEvent(new CustomEvent('game:extra-turn-remaining', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:players-skipped
|
||||
- **Purpose:** Broadcasts when players are skipped due to lost turn consequences
|
||||
- **Action:** Emits event for UI notification
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:players-skipped', (data) => {
|
||||
log('⏭️ Players skipped:', data.skippedPlayers);
|
||||
window.dispatchEvent(new CustomEvent('game:players-skipped', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ game:cleanup-complete
|
||||
- **Purpose:** Confirms cleanup after game end
|
||||
- **Action:** Emits event for final UI state reset
|
||||
- **Implementation:**
|
||||
```javascript
|
||||
socket.on('game:cleanup-complete', (data) => {
|
||||
log('🧹 Cleanup complete:', data);
|
||||
window.dispatchEvent(new CustomEvent('game:cleanup-complete', { detail: data }));
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. GameWebSocketContext.jsx - Added Missing Action Methods
|
||||
|
||||
Added 4 critical action methods that players and gamemaster need:
|
||||
|
||||
#### ✅ submitAnswer(answer)
|
||||
- **Purpose:** Submit answer to question card
|
||||
- **Parameters:** `answer` - Player's answer (type depends on card type: string for QUIZ/OWN_ANSWER, array for SENTENCE_PAIRING, boolean for TRUE_FALSE, number for CLOSER)
|
||||
- **Emits:** `game:card-answer` with gameCode and answer
|
||||
- **Returns:** Boolean (success/failure)
|
||||
|
||||
```javascript
|
||||
const submitAnswer = useCallback((answer) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot submit answer: not connected');
|
||||
return false;
|
||||
}
|
||||
log('📝 Submitting answer:', answer);
|
||||
socket.emit('game:card-answer', { gameCode: gameState?.gameCode, answer });
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
```
|
||||
|
||||
#### ✅ submitPositionGuess(guessedPosition)
|
||||
- **Purpose:** Submit position guess after correct answer
|
||||
- **Parameters:** `guessedPosition` - Number (0-99) representing guessed board position
|
||||
- **Emits:** `game:position-guess` with gameCode and guessedPosition
|
||||
- **Returns:** Boolean (success/failure)
|
||||
|
||||
```javascript
|
||||
const submitPositionGuess = useCallback((guessedPosition) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot submit position guess: not connected');
|
||||
return false;
|
||||
}
|
||||
log('🎯 Submitting position guess:', guessedPosition);
|
||||
socket.emit('game:position-guess', { gameCode: gameState?.gameCode, guessedPosition });
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
```
|
||||
|
||||
#### ✅ approveJoker(requestId)
|
||||
- **Purpose:** Gamemaster approves joker card
|
||||
- **Parameters:** `requestId` - Unique identifier for joker decision request
|
||||
- **Emits:** `game:gamemaster-decision` with gameCode, requestId, decision: 'approve'
|
||||
- **Returns:** Boolean (success/failure)
|
||||
- **Authorization:** Requires isGamemaster = true
|
||||
|
||||
```javascript
|
||||
const approveJoker = useCallback((requestId) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected || !isGamemaster) {
|
||||
warn('⚠️ Cannot approve joker: not gamemaster or not connected');
|
||||
return false;
|
||||
}
|
||||
log('✅ Approving joker request:', requestId);
|
||||
socket.emit('game:gamemaster-decision', {
|
||||
gameCode: gameState?.gameCode,
|
||||
requestId,
|
||||
decision: 'approve'
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, isGamemaster, gameState?.gameCode]);
|
||||
```
|
||||
|
||||
#### ✅ rejectJoker(requestId, reason?)
|
||||
- **Purpose:** Gamemaster rejects joker card
|
||||
- **Parameters:**
|
||||
- `requestId` - Unique identifier for joker decision request
|
||||
- `reason` - Optional rejection reason (default: 'Joker answer rejected')
|
||||
- **Emits:** `game:gamemaster-decision` with gameCode, requestId, decision: 'reject', reason
|
||||
- **Returns:** Boolean (success/failure)
|
||||
- **Authorization:** Requires isGamemaster = true
|
||||
|
||||
```javascript
|
||||
const rejectJoker = useCallback((requestId, reason = 'Joker answer rejected') => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected || !isGamemaster) {
|
||||
warn('⚠️ Cannot reject joker: not gamemaster or not connected');
|
||||
return false;
|
||||
}
|
||||
log('❌ Rejecting joker request:', requestId, 'Reason:', reason);
|
||||
socket.emit('game:gamemaster-decision', {
|
||||
gameCode: gameState?.gameCode,
|
||||
requestId,
|
||||
decision: 'reject',
|
||||
reason
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, isGamemaster, gameState?.gameCode]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. GameWebSocketContext.jsx - Updated Context Value Export
|
||||
|
||||
Updated the context value to export all new methods:
|
||||
|
||||
```javascript
|
||||
const value = {
|
||||
socket: socketRef.current,
|
||||
isConnected,
|
||||
gameState,
|
||||
players,
|
||||
boardData,
|
||||
currentTurn,
|
||||
error,
|
||||
isGamemaster,
|
||||
gameStarted,
|
||||
pendingPlayers,
|
||||
approvalStatus,
|
||||
// Connection management
|
||||
connect,
|
||||
disconnect,
|
||||
// Methods
|
||||
rollDice,
|
||||
sendMessage,
|
||||
setReady,
|
||||
leaveGame,
|
||||
approvePlayer,
|
||||
rejectPlayer,
|
||||
submitAnswer, // ✅ NEW
|
||||
submitPositionGuess, // ✅ NEW
|
||||
approveJoker, // ✅ NEW
|
||||
rejectJoker, // ✅ NEW
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. GameScreen.jsx - Fixed Action Method Calls
|
||||
|
||||
**Location:** `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx`
|
||||
|
||||
#### Fixed handleSubmitAnswer
|
||||
**Before:**
|
||||
```javascript
|
||||
const handleSubmitAnswer = useCallback((answer) => {
|
||||
if (currentCard?.id) {
|
||||
submitAnswer(currentCard.id, answer) // ❌ Wrong parameters
|
||||
}
|
||||
}, [currentCard?.id, submitAnswer])
|
||||
```
|
||||
|
||||
**After:**
|
||||
```javascript
|
||||
const handleSubmitAnswer = useCallback((answer) => {
|
||||
console.log('📝 Válasz beküldve:', answer)
|
||||
submitAnswer(answer) // ✅ Correct - backend extracts gameCode from context
|
||||
}, [submitAnswer])
|
||||
```
|
||||
|
||||
#### Fixed handleApproveJoker
|
||||
**Before:**
|
||||
```javascript
|
||||
const handleApproveJoker = useCallback(async (jokerRequest) => {
|
||||
approveJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId) // ❌ Wrong parameters
|
||||
setIsJokerModalOpen(false)
|
||||
}, [approveJoker])
|
||||
```
|
||||
|
||||
**After:**
|
||||
```javascript
|
||||
const handleApproveJoker = useCallback(async (jokerRequest) => {
|
||||
console.log('✅ Joker feladat jóváhagyva:', jokerRequest)
|
||||
approveJoker(jokerRequest.requestId) // ✅ Correct - only requestId needed
|
||||
setIsJokerModalOpen(false)
|
||||
}, [approveJoker])
|
||||
```
|
||||
|
||||
#### Fixed handleRejectJoker
|
||||
**Before:**
|
||||
```javascript
|
||||
const handleRejectJoker = useCallback(async (jokerRequest) => {
|
||||
rejectJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId) // ❌ Wrong parameters
|
||||
setIsJokerModalOpen(false)
|
||||
}, [rejectJoker])
|
||||
```
|
||||
|
||||
**After:**
|
||||
```javascript
|
||||
const handleRejectJoker = useCallback(async (jokerRequest) => {
|
||||
console.log('❌ Joker feladat elutasítva:', jokerRequest)
|
||||
rejectJoker(jokerRequest.requestId, 'Joker rejected by gamemaster') // ✅ Correct
|
||||
setIsJokerModalOpen(false)
|
||||
}, [rejectJoker])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
### ✅ Centralized Event Handling
|
||||
All WebSocket events are handled in the context, ensuring:
|
||||
- Single source of truth for game state
|
||||
- Consistent state updates across all components
|
||||
- Easy debugging with centralized logging
|
||||
|
||||
### ✅ Custom Event Bridge
|
||||
Events are re-emitted as CustomEvents via `window.dispatchEvent()`, allowing:
|
||||
- GameScreen to add specific UI logic without modifying context
|
||||
- Separation of concerns (state management vs UI presentation)
|
||||
- Multiple components can listen to the same events independently
|
||||
|
||||
### ✅ Persistent Connection
|
||||
Socket connection persists across navigation (Lobby → GameScreen), ensuring:
|
||||
- No disconnections during page transitions
|
||||
- Gamemaster can start game without socket dropping
|
||||
- Real-time updates continue seamlessly
|
||||
|
||||
### ✅ Type Safety & Validation
|
||||
All action methods include:
|
||||
- Connection state checks (`isConnected`)
|
||||
- Authorization checks (`isGamemaster` for approval methods)
|
||||
- Error logging for debugging
|
||||
- Boolean return values for success/failure
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### ✅ Event Handler Tests
|
||||
- [ ] Test `game:your-turn` - Turn indicator updates
|
||||
- [ ] Test `game:dice-rolled` - Dice animation triggers
|
||||
- [ ] Test `game:guess-result` - Position updates on correct guess
|
||||
- [ ] Test `game:joker-complete` - Position updates on approved joker
|
||||
- [ ] Test `game:luck-consequence` - Position updates from luck cards
|
||||
- [ ] Test `game:ended` - Winner modal displays with final scores
|
||||
- [ ] Test `game:extra-turn-remaining` - Extra turn notification
|
||||
- [ ] Test `game:players-skipped` - Skip notification
|
||||
- [ ] Test `game:cleanup-complete` - Cleanup confirmation
|
||||
|
||||
### ✅ Action Method Tests
|
||||
- [ ] Test `submitAnswer()` - Answer submission for all card types (QUIZ, SENTENCE_PAIRING, TRUE_FALSE, CLOSER, OWN_ANSWER)
|
||||
- [ ] Test `submitPositionGuess()` - Position guess submission
|
||||
- [ ] Test `approveJoker()` - Gamemaster approval (requires isGamemaster)
|
||||
- [ ] Test `rejectJoker()` - Gamemaster rejection (requires isGamemaster)
|
||||
|
||||
### ✅ Integration Tests
|
||||
- [ ] Complete game flow: Start → Dice → Card → Answer → Position Guess → Next Turn
|
||||
- [ ] Joker flow: Joker drawn → Request sent → Gamemaster decision → Position update
|
||||
- [ ] Luck flow: Luck card → Consequence applied → Position/turn updated
|
||||
- [ ] End game flow: Player reaches finish → Winner announced → Scores displayed
|
||||
|
||||
---
|
||||
|
||||
## Remaining UI Enhancements
|
||||
|
||||
### 🎨 Turn Indicator Component
|
||||
**Status:** Not implemented
|
||||
**Description:** Visual indicator showing whose turn it is
|
||||
**Events:** `game:your-turn`, `game:turn-changed`
|
||||
**Location:** GameScreen.jsx header area
|
||||
|
||||
### ⏱️ Timer Component
|
||||
**Status:** Not implemented
|
||||
**Description:** Countdown timer for card answers (60s) and joker decisions (120s)
|
||||
**Events:** `game:card-drawn-self`, `game:gamemaster-decision-request`
|
||||
**Location:** CardDisplayModal, JokerApprovalModal
|
||||
|
||||
### 🏆 Winner Modal
|
||||
**Status:** Not implemented
|
||||
**Description:** Full-screen modal showing winner, final scores, and play again option
|
||||
**Events:** `game:ended`
|
||||
**Location:** GameScreen.jsx (new modal component)
|
||||
|
||||
### ✨ Position Update Animations
|
||||
**Status:** Not implemented
|
||||
**Description:** Smooth token movement animations for position changes
|
||||
**Events:** `game:player-moved`, `game:guess-result`, `game:joker-complete`, `game:luck-consequence`
|
||||
**Location:** GameScreen.jsx player token rendering
|
||||
|
||||
### 📊 Score Display
|
||||
**Status:** Not implemented
|
||||
**Description:** Live leaderboard showing player rankings
|
||||
**State:** `players` array with position data
|
||||
**Location:** GameScreen.jsx sidebar or header
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Future Work
|
||||
|
||||
### 🐛 Known Issues
|
||||
None currently - all core functionality implemented and error-free.
|
||||
|
||||
### 🚀 Future Enhancements
|
||||
1. **Notification System** - Toast/notification UI for game events
|
||||
2. **Sound Effects** - Audio feedback for dice, cards, turns
|
||||
3. **Animation Polish** - Smooth transitions for all state changes
|
||||
4. **Mobile Responsiveness** - Touch-friendly controls for mobile devices
|
||||
5. **Accessibility** - ARIA labels, keyboard navigation, screen reader support
|
||||
6. **Reconnection Logic** - Handle network interruptions gracefully
|
||||
7. **Spectator Mode** - Allow non-playing users to watch games
|
||||
8. **Chat System** - Player communication during game
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
✅ **9 critical event handlers** added to GameWebSocketContext
|
||||
✅ **4 essential action methods** added to GameWebSocketContext
|
||||
✅ **3 handler fixes** in GameScreen for correct parameter usage
|
||||
✅ **Zero compilation errors** - all changes validated
|
||||
✅ **Full gameplay flow** now supported by frontend
|
||||
|
||||
The frontend is now **functionally complete** for core gameplay. Players can:
|
||||
- Receive turn notifications
|
||||
- Roll dice and move
|
||||
- Draw and answer cards
|
||||
- Submit position guesses
|
||||
- Complete joker challenges (with gamemaster approval)
|
||||
- Experience luck consequences
|
||||
- See game end with winner announcement
|
||||
|
||||
Remaining work is **UI polish** (animations, timers, winner screen) rather than functional gaps.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** November 19, 2025
|
||||
**Next Steps:** Implement UI enhancements and run comprehensive integration tests.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
||||
# pgAdmin Database Administration Guide
|
||||
|
||||
## Access pgAdmin
|
||||
|
||||
- **URL**: http://localhost:8080
|
||||
- **Email**: admin@serpentrace.dev
|
||||
- **Password**: admin
|
||||
|
||||
## Pre-configured Server
|
||||
|
||||
The pgAdmin interface should have a pre-configured server named **"SerpentRace PostgreSQL Dev"** in the "Development" group.
|
||||
|
||||
## Manual Server Configuration (If Needed)
|
||||
|
||||
If the server is not automatically configured, add it manually:
|
||||
|
||||
### Server Details
|
||||
- **Name**: SerpentRace PostgreSQL Dev
|
||||
- **Host**: postgres (or localhost if connecting from outside Docker)
|
||||
- **Port**: 5432
|
||||
- **Database**: serpentrace
|
||||
- **Username**: postgres
|
||||
- **Password**: postgres
|
||||
|
||||
### Steps to Add Server Manually
|
||||
|
||||
1. Right-click on "Servers" in the left panel
|
||||
2. Select "Register" > "Server..."
|
||||
3. Fill in the "General" tab:
|
||||
- Name: `SerpentRace PostgreSQL Dev`
|
||||
- Server group: `Development`
|
||||
4. Fill in the "Connection" tab:
|
||||
- Host name/address: `postgres`
|
||||
- Port: `5432`
|
||||
- Maintenance database: `serpentrace`
|
||||
- Username: `postgres`
|
||||
- Password: `postgres`
|
||||
5. Click "Save"
|
||||
|
||||
## Common Database Operations
|
||||
|
||||
### View Tables
|
||||
1. Expand the server connection
|
||||
2. Expand "Databases" > "serpentrace"
|
||||
3. Expand "Schemas" > "public"
|
||||
4. Expand "Tables"
|
||||
|
||||
### Run SQL Queries
|
||||
1. Right-click on the database name
|
||||
2. Select "Query Tool"
|
||||
3. Write your SQL queries in the editor
|
||||
4. Click the "Execute" button or press F5
|
||||
|
||||
### View Data
|
||||
1. Right-click on any table
|
||||
2. Select "View/Edit Data" > "All Rows"
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
- Ensure Docker containers are running: `docker ps`
|
||||
- Check container logs: `docker logs serpentrace-postgres-dev`
|
||||
- Test connections: `npm run test:connections`
|
||||
|
||||
### Authentication Failed
|
||||
- Verify the password is correct: `postgres`
|
||||
- Check if you're using the correct hostname: `postgres` (inside Docker) vs `localhost` (outside Docker)
|
||||
|
||||
### Server Not Appearing
|
||||
- Restart pgAdmin container:
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml restart pgadmin
|
||||
```
|
||||
- Clear browser cache and reload
|
||||
|
||||
## Development Tips
|
||||
|
||||
### Useful SQL Queries
|
||||
|
||||
```sql
|
||||
-- List all tables
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public';
|
||||
|
||||
-- Check database size
|
||||
SELECT pg_size_pretty(pg_database_size('serpentrace'));
|
||||
|
||||
-- View active connections
|
||||
SELECT * FROM pg_stat_activity WHERE datname = 'serpentrace';
|
||||
|
||||
-- Check migration status (if using TypeORM)
|
||||
SELECT * FROM migrations ORDER BY timestamp DESC;
|
||||
```
|
||||
|
||||
### Database Backup
|
||||
1. Right-click on database name
|
||||
2. Select "Backup..."
|
||||
3. Choose format (Custom recommended for pgAdmin restore)
|
||||
4. Set filename and location
|
||||
5. Click "Backup"
|
||||
|
||||
### Database Restore
|
||||
1. Right-click on "Databases"
|
||||
2. Select "Restore..."
|
||||
3. Choose the backup file
|
||||
4. Configure options as needed
|
||||
5. Click "Restore"
|
||||
|
||||
## Security Notes
|
||||
|
||||
⚠️ **Development Only**: The current configuration uses default credentials and is intended for development only. For production:
|
||||
|
||||
- Use strong, unique passwords
|
||||
- Enable SSL connections
|
||||
- Restrict network access
|
||||
- Use environment variables for credentials
|
||||
- Enable authentication and authorization features
|
||||
-510
@@ -1,510 +0,0 @@
|
||||
/*!
|
||||
* /**
|
||||
* * Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* *
|
||||
* * This source code is licensed under the MIT license found in the
|
||||
* * LICENSE file in the root directory of this source tree.
|
||||
* * /
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ "./src/runTest.ts":
|
||||
/***/ ((__unused_webpack_module, exports) => {
|
||||
|
||||
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({
|
||||
value: true
|
||||
}));
|
||||
exports["default"] = runTest;
|
||||
function _nodeVm() {
|
||||
const data = require("node:vm");
|
||||
_nodeVm = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _chalk() {
|
||||
const data = _interopRequireDefault(require("chalk"));
|
||||
_chalk = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function fs() {
|
||||
const data = _interopRequireWildcard(require("graceful-fs"));
|
||||
fs = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function sourcemapSupport() {
|
||||
const data = _interopRequireWildcard(require("source-map-support"));
|
||||
sourcemapSupport = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _console() {
|
||||
const data = require("@jest/console");
|
||||
_console = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _transform() {
|
||||
const data = require("@jest/transform");
|
||||
_transform = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function docblock() {
|
||||
const data = _interopRequireWildcard(require("jest-docblock"));
|
||||
docblock = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestLeakDetector() {
|
||||
const data = _interopRequireDefault(require("jest-leak-detector"));
|
||||
_jestLeakDetector = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestMessageUtil() {
|
||||
const data = require("jest-message-util");
|
||||
_jestMessageUtil = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestResolve() {
|
||||
const data = require("jest-resolve");
|
||||
_jestResolve = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestUtil() {
|
||||
const data = require("jest-util");
|
||||
_jestUtil = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
||||
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
|
||||
function freezeConsole(testConsole, config) {
|
||||
// @ts-expect-error: `_log` is `private` - we should figure out some proper API here
|
||||
testConsole._log = function fakeConsolePush(_type, message) {
|
||||
const error = new (_jestUtil().ErrorWithStack)(`${_chalk().default.red(`${_chalk().default.bold('Cannot log after tests are done.')} Did you forget to wait for something async in your test?`)}\nAttempted to log "${message}".`, fakeConsolePush);
|
||||
const formattedError = (0, _jestMessageUtil().formatExecError)(error, config, {
|
||||
noStackTrace: false
|
||||
}, undefined, true);
|
||||
process.stderr.write(`\n${formattedError}\n`);
|
||||
process.exitCode = 1;
|
||||
};
|
||||
}
|
||||
|
||||
// Keeping the core of "runTest" as a separate function (as "runTestInternal")
|
||||
// is key to be able to detect memory leaks. Since all variables are local to
|
||||
// the function, when "runTestInternal" finishes its execution, they can all be
|
||||
// freed, UNLESS something else is leaking them (and that's why we can detect
|
||||
// the leak!).
|
||||
//
|
||||
// If we had all the code in a single function, we should manually nullify all
|
||||
// references to verify if there is a leak, which is not maintainable and error
|
||||
// prone. That's why "runTestInternal" CANNOT be inlined inside "runTest".
|
||||
async function runTestInternal(path, globalConfig, projectConfig, resolver, context, sendMessageToJest) {
|
||||
const testSource = fs().readFileSync(path, 'utf8');
|
||||
const docblockPragmas = docblock().parse(docblock().extract(testSource));
|
||||
const customEnvironment = docblockPragmas['jest-environment'];
|
||||
const loadTestEnvironmentStart = Date.now();
|
||||
let testEnvironment = projectConfig.testEnvironment;
|
||||
if (customEnvironment) {
|
||||
if (Array.isArray(customEnvironment)) {
|
||||
throw new TypeError(`You can only define a single test environment through docblocks, got "${customEnvironment.join(', ')}"`);
|
||||
}
|
||||
testEnvironment = (0, _jestResolve().resolveTestEnvironment)({
|
||||
...projectConfig,
|
||||
// we wanna avoid webpack trying to be clever
|
||||
requireResolveFunction: module => require.resolve(module),
|
||||
testEnvironment: customEnvironment
|
||||
});
|
||||
}
|
||||
const cacheFS = new Map([[path, testSource]]);
|
||||
const transformer = await (0, _transform().createScriptTransformer)(projectConfig, cacheFS);
|
||||
const TestEnvironment = await transformer.requireAndTranspileModule(testEnvironment);
|
||||
const testFramework = await transformer.requireAndTranspileModule(process.env.JEST_JASMINE === '1' ? require.resolve('jest-jasmine2') : projectConfig.testRunner);
|
||||
const Runtime = (0, _jestUtil().interopRequireDefault)(projectConfig.runtime ? require(projectConfig.runtime) : require('jest-runtime')).default;
|
||||
const consoleOut = globalConfig.useStderr ? process.stderr : process.stdout;
|
||||
const consoleFormatter = (type, message) => (0, _console().getConsoleOutput)(
|
||||
// 4 = the console call is buried 4 stack frames deep
|
||||
_console().BufferedConsole.write([], type, message, 4), projectConfig, globalConfig);
|
||||
let testConsole;
|
||||
if (globalConfig.silent) {
|
||||
testConsole = new (_console().NullConsole)(consoleOut, consoleOut, consoleFormatter);
|
||||
} else if (globalConfig.verbose) {
|
||||
testConsole = new (_console().CustomConsole)(consoleOut, consoleOut, consoleFormatter);
|
||||
} else {
|
||||
testConsole = new (_console().BufferedConsole)();
|
||||
}
|
||||
let extraTestEnvironmentOptions;
|
||||
const docblockEnvironmentOptions = docblockPragmas['jest-environment-options'];
|
||||
if (typeof docblockEnvironmentOptions === 'string') {
|
||||
extraTestEnvironmentOptions = JSON.parse(docblockEnvironmentOptions);
|
||||
}
|
||||
const environment = new TestEnvironment({
|
||||
globalConfig,
|
||||
projectConfig: extraTestEnvironmentOptions ? {
|
||||
...projectConfig,
|
||||
testEnvironmentOptions: {
|
||||
...projectConfig.testEnvironmentOptions,
|
||||
...extraTestEnvironmentOptions
|
||||
}
|
||||
} : projectConfig
|
||||
}, {
|
||||
console: testConsole,
|
||||
docblockPragmas,
|
||||
testPath: path
|
||||
});
|
||||
const loadTestEnvironmentEnd = Date.now();
|
||||
if (typeof environment.getVmContext !== 'function') {
|
||||
console.error(`Test environment found at "${testEnvironment}" does not export a "getVmContext" method, which is mandatory from Jest 27. This method is a replacement for "runScript".`);
|
||||
process.exit(1);
|
||||
}
|
||||
const leakDetector = projectConfig.detectLeaks ? new (_jestLeakDetector().default)(environment) : null;
|
||||
(0, _jestUtil().setGlobal)(environment.global, 'console', testConsole, 'retain');
|
||||
const runtime = new Runtime(projectConfig, environment, resolver, transformer, cacheFS, {
|
||||
changedFiles: context.changedFiles,
|
||||
collectCoverage: globalConfig.collectCoverage,
|
||||
collectCoverageFrom: globalConfig.collectCoverageFrom,
|
||||
coverageProvider: globalConfig.coverageProvider,
|
||||
sourcesRelatedToTestsInChangedFiles: context.sourcesRelatedToTestsInChangedFiles
|
||||
}, path, globalConfig);
|
||||
let isTornDown = false;
|
||||
const tearDownEnv = async () => {
|
||||
if (!isTornDown) {
|
||||
runtime.teardown();
|
||||
|
||||
// source-map-support keeps memory leftovers in `Error.prepareStackTrace`
|
||||
(0, _nodeVm().runInContext)("Error.prepareStackTrace = () => '';", environment.getVmContext());
|
||||
sourcemapSupport().resetRetrieveHandlers();
|
||||
try {
|
||||
await environment.teardown();
|
||||
} finally {
|
||||
isTornDown = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
const start = Date.now();
|
||||
const setupFilesStart = Date.now();
|
||||
for (const path of projectConfig.setupFiles) {
|
||||
const esm = runtime.unstable_shouldLoadAsEsm(path);
|
||||
if (esm) {
|
||||
await runtime.unstable_importModule(path);
|
||||
} else {
|
||||
const setupFile = runtime.requireModule(path);
|
||||
if (typeof setupFile === 'function') {
|
||||
await setupFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
const setupFilesEnd = Date.now();
|
||||
const sourcemapOptions = {
|
||||
environment: 'node',
|
||||
handleUncaughtExceptions: false,
|
||||
retrieveSourceMap: source => {
|
||||
const sourceMapSource = runtime.getSourceMaps()?.get(source);
|
||||
if (sourceMapSource) {
|
||||
try {
|
||||
return {
|
||||
map: JSON.parse(fs().readFileSync(sourceMapSource, 'utf8')),
|
||||
url: source
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// For tests
|
||||
runtime.requireInternalModule(require.resolve('source-map-support')).install(sourcemapOptions);
|
||||
|
||||
// For runtime errors
|
||||
sourcemapSupport().install(sourcemapOptions);
|
||||
if (environment.global && environment.global.process && environment.global.process.exit) {
|
||||
const realExit = environment.global.process.exit;
|
||||
environment.global.process.exit = function exit(...args) {
|
||||
const error = new (_jestUtil().ErrorWithStack)(`process.exit called with "${args.join(', ')}"`, exit);
|
||||
const formattedError = (0, _jestMessageUtil().formatExecError)(error, projectConfig, {
|
||||
noStackTrace: false
|
||||
}, undefined, true);
|
||||
process.stderr.write(formattedError);
|
||||
return realExit(...args);
|
||||
};
|
||||
}
|
||||
|
||||
// if we don't have `getVmContext` on the env skip coverage
|
||||
const collectV8Coverage = globalConfig.collectCoverage && globalConfig.coverageProvider === 'v8' && typeof environment.getVmContext === 'function';
|
||||
|
||||
// Node's error-message stack size is limited at 10, but it's pretty useful
|
||||
// to see more than that when a test fails.
|
||||
Error.stackTraceLimit = 100;
|
||||
try {
|
||||
await environment.setup();
|
||||
let result;
|
||||
try {
|
||||
if (collectV8Coverage) {
|
||||
await runtime.collectV8Coverage();
|
||||
}
|
||||
result = await testFramework(globalConfig, projectConfig, environment, runtime, path, sendMessageToJest);
|
||||
} catch (error) {
|
||||
// Access all stacks before uninstalling sourcemaps
|
||||
let e = error;
|
||||
while (typeof e === 'object' && e !== null && 'stack' in e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
e.stack;
|
||||
e = e?.cause;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (collectV8Coverage) {
|
||||
await runtime.stopCollectingV8Coverage();
|
||||
}
|
||||
}
|
||||
freezeConsole(testConsole, projectConfig);
|
||||
const testCount = result.numPassingTests + result.numFailingTests + result.numPendingTests + result.numTodoTests;
|
||||
const end = Date.now();
|
||||
const testRuntime = end - start;
|
||||
result.perfStats = {
|
||||
...result.perfStats,
|
||||
end,
|
||||
loadTestEnvironmentEnd,
|
||||
loadTestEnvironmentStart,
|
||||
runtime: testRuntime,
|
||||
setupFilesEnd,
|
||||
setupFilesStart,
|
||||
slow: testRuntime / 1000 > projectConfig.slowTestThreshold,
|
||||
start
|
||||
};
|
||||
result.testFilePath = path;
|
||||
result.console = testConsole.getBuffer();
|
||||
result.skipped = testCount === result.numPendingTests;
|
||||
result.displayName = projectConfig.displayName;
|
||||
const coverage = runtime.getAllCoverageInfoCopy();
|
||||
if (coverage) {
|
||||
const coverageKeys = Object.keys(coverage);
|
||||
if (coverageKeys.length > 0) {
|
||||
result.coverage = coverage;
|
||||
}
|
||||
}
|
||||
if (collectV8Coverage) {
|
||||
const v8Coverage = runtime.getAllV8CoverageInfoCopy();
|
||||
if (v8Coverage && v8Coverage.length > 0) {
|
||||
result.v8Coverage = v8Coverage;
|
||||
}
|
||||
}
|
||||
if (globalConfig.logHeapUsage) {
|
||||
globalThis.gc?.();
|
||||
result.memoryUsage = process.memoryUsage().heapUsed;
|
||||
}
|
||||
await tearDownEnv();
|
||||
|
||||
// Delay the resolution to allow log messages to be output.
|
||||
return await new Promise(resolve => {
|
||||
setImmediate(() => resolve({
|
||||
leakDetector,
|
||||
result
|
||||
}));
|
||||
});
|
||||
} finally {
|
||||
await tearDownEnv();
|
||||
}
|
||||
}
|
||||
async function runTest(path, globalConfig, config, resolver, context, sendMessageToJest) {
|
||||
const {
|
||||
leakDetector,
|
||||
result
|
||||
} = await runTestInternal(path, globalConfig, config, resolver, context, sendMessageToJest);
|
||||
if (leakDetector) {
|
||||
// We wanna allow a tiny but time to pass to allow last-minute cleanup
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Resolve leak detector, outside the "runTestInternal" closure.
|
||||
result.leaks = await leakDetector.isLeaking();
|
||||
} else {
|
||||
result.leaks = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/ // The module cache
|
||||
/******/ var __webpack_module_cache__ = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/ // Check if module is in cache
|
||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||
/******/ if (cachedModule !== undefined) {
|
||||
/******/ return cachedModule.exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||
/******/ // no module.id needed
|
||||
/******/ // no module.loaded needed
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/************************************************************************/
|
||||
var __webpack_exports__ = {};
|
||||
// This entry needs to be wrapped in an IIFE because it uses a non-standard name for the exports (exports).
|
||||
(() => {
|
||||
var exports = __webpack_exports__;
|
||||
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({
|
||||
value: true
|
||||
}));
|
||||
exports.setup = setup;
|
||||
exports.worker = worker;
|
||||
function _exitX() {
|
||||
const data = _interopRequireDefault(require("exit-x"));
|
||||
_exitX = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestHasteMap() {
|
||||
const data = _interopRequireDefault(require("jest-haste-map"));
|
||||
_jestHasteMap = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestMessageUtil() {
|
||||
const data = require("jest-message-util");
|
||||
_jestMessageUtil = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestRuntime() {
|
||||
const data = _interopRequireDefault(require("jest-runtime"));
|
||||
_jestRuntime = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
function _jestWorker() {
|
||||
const data = require("jest-worker");
|
||||
_jestWorker = function () {
|
||||
return data;
|
||||
};
|
||||
return data;
|
||||
}
|
||||
var _runTest = _interopRequireDefault(__webpack_require__("./src/runTest.ts"));
|
||||
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
// Make sure uncaught errors are logged before we exit.
|
||||
process.on('uncaughtException', err => {
|
||||
if (err.stack) {
|
||||
console.error(err.stack);
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
(0, _exitX().default)(1);
|
||||
});
|
||||
const formatError = error => {
|
||||
if (typeof error === 'string') {
|
||||
const {
|
||||
message,
|
||||
stack
|
||||
} = (0, _jestMessageUtil().separateMessageFromStack)(error);
|
||||
return {
|
||||
message,
|
||||
stack,
|
||||
type: 'Error'
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: error.code || undefined,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
type: 'Error'
|
||||
};
|
||||
};
|
||||
const resolvers = new Map();
|
||||
const getResolver = config => {
|
||||
const resolver = resolvers.get(config.id);
|
||||
if (!resolver) {
|
||||
throw new Error(`Cannot find resolver for: ${config.id}`);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
function setup(setupData) {
|
||||
// Module maps that will be needed for the test runs are passed.
|
||||
for (const {
|
||||
config,
|
||||
serializableModuleMap
|
||||
} of setupData.serializableResolvers) {
|
||||
const moduleMap = _jestHasteMap().default.getStatic(config).getModuleMapFromJSON(serializableModuleMap);
|
||||
resolvers.set(config.id, _jestRuntime().default.createResolver(config, moduleMap));
|
||||
}
|
||||
}
|
||||
const sendMessageToJest = (eventName, args) => {
|
||||
(0, _jestWorker().messageParent)([eventName, args]);
|
||||
};
|
||||
async function worker({
|
||||
config,
|
||||
globalConfig,
|
||||
path,
|
||||
context
|
||||
}) {
|
||||
try {
|
||||
return await (0, _runTest.default)(path, globalConfig, config, getResolver(config), {
|
||||
...context,
|
||||
changedFiles: context.changedFiles && new Set(context.changedFiles),
|
||||
sourcesRelatedToTestsInChangedFiles: context.sourcesRelatedToTestsInChangedFiles && new Set(context.sourcesRelatedToTestsInChangedFiles)
|
||||
}, sendMessageToJest);
|
||||
} catch (error) {
|
||||
throw formatError(error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
module.exports = __webpack_exports__;
|
||||
/******/ })()
|
||||
;
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const importLocal = require('import-local');
|
||||
|
||||
if (!importLocal(__filename)) {
|
||||
require('jest-cli/bin/jest');
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import helmet from 'helmet';
|
||||
import { AppDataSource } from '../Infrastructure/ormconfig';
|
||||
import userRouter from './routers/userRouter';
|
||||
import organizationRouter from './routers/organizationRouter';
|
||||
import deckRouter from './routers/deckRouter';
|
||||
import chatRouter from './routers/chatRouter';
|
||||
import contactRouter from './routers/contactRouter';
|
||||
import adminRouter from './routers/adminRouter';
|
||||
import deckImportExportRouter from './routers/deckImportExportRouter';
|
||||
import gameRouter from './routers/gameRouter';
|
||||
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
|
||||
import { WebSocketService } from '../Application/Services/WebSocketService';
|
||||
import { GameWebSocketService } from '../Application/Services/GameWebSocketService';
|
||||
import { container } from '../Application/Services/DIContainer';
|
||||
import { GameRepository } from '../Infrastructure/Repository/GameRepository';
|
||||
import { UserRepository } from '../Infrastructure/Repository/UserRepository';
|
||||
import { RedisService } from '../Application/Services/RedisService';
|
||||
import { setupSwagger } from './swagger/swaggerUiSetup';
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
const loggingService = LoggingService.getInstance();
|
||||
|
||||
logStartup('SerpentRace Backend starting up', {
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
port: PORT,
|
||||
nodeVersion: process.version,
|
||||
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
|
||||
});
|
||||
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: isDevelopment ? false : undefined
|
||||
}));
|
||||
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(loggingService.requestLoggingMiddleware());
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080', process.env.FRONTEND_URL];
|
||||
|
||||
if (!origin || allowedOrigins.includes(origin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
||||
}
|
||||
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
if (isDevelopment) {
|
||||
app.use((req, res, next) => {
|
||||
logRequest(`${req.method} ${req.path}`, req, res);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Setup Swagger documentation
|
||||
setupSwagger(app);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
service: 'SerpentRace Backend API',
|
||||
status: 'running',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
swagger: '/api-docs',
|
||||
users: '/api/users',
|
||||
organizations: '/api/organizations',
|
||||
decks: '/api/decks',
|
||||
chats: '/api/chats',
|
||||
contacts: '/api/contacts',
|
||||
admin: '/api/admin',
|
||||
deckImportExport: '/api/deck-import-export',
|
||||
health: '/health'
|
||||
},
|
||||
websocket: {
|
||||
enabled: true,
|
||||
events: [
|
||||
'chat:join', 'chat:leave', 'message:send',
|
||||
'group:create', 'chat:direct', 'game:chat:create',
|
||||
'chat:history'
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/health', async (req, res) => {
|
||||
try {
|
||||
const isDbConnected = AppDataSource.isInitialized;
|
||||
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'SerpentRace Backend API',
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
database: {
|
||||
connected: isDbConnected,
|
||||
type: AppDataSource.options.type
|
||||
},
|
||||
websocket: {
|
||||
enabled: true
|
||||
},
|
||||
uptime: process.uptime()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Service health check failed'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API Routes
|
||||
app.use('/api/users', userRouter);
|
||||
app.use('/api/organizations', organizationRouter);
|
||||
app.use('/api/decks', deckRouter);
|
||||
app.use('/api/chats', chatRouter);
|
||||
app.use('/api/contacts', contactRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/deck-import-export', deckImportExportRouter);
|
||||
app.use('/api/games', gameRouter);
|
||||
|
||||
// Global error handler (must be after routes)
|
||||
app.use(loggingService.errorLoggingMiddleware());
|
||||
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logError('Global error handler caught unhandled error', error, req, res);
|
||||
|
||||
// Don't expose internal error details in production
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
timestamp: new Date().toISOString(),
|
||||
...(isDevelopment && { details: error.message, stack: error.stack })
|
||||
});
|
||||
});
|
||||
|
||||
// Handle 404 routes
|
||||
app.use((req: express.Request, res: express.Response) => {
|
||||
res.status(404).json({
|
||||
error: 'Route not found',
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize WebSocket service after database connection
|
||||
let webSocketService: WebSocketService;
|
||||
let gameWebSocketService: GameWebSocketService;
|
||||
let server: any; // Declare server variable
|
||||
|
||||
// Initialize database connection and start server
|
||||
AppDataSource.initialize()
|
||||
.then(() => {
|
||||
const dbOptions = AppDataSource.options as any;
|
||||
logConnection('Database connection established', 'postgresql', 'success', {
|
||||
type: dbOptions.type,
|
||||
host: dbOptions.host,
|
||||
database: dbOptions.database
|
||||
});
|
||||
|
||||
// Initialize WebSocket service after database is connected
|
||||
webSocketService = new WebSocketService(httpServer);
|
||||
logStartup('WebSocket service initialized', {
|
||||
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
|
||||
});
|
||||
|
||||
// Initialize Game WebSocket service for /game namespace via DIContainer
|
||||
container.setSocketIO(webSocketService['io']);
|
||||
gameWebSocketService = container.gameWebSocketService;
|
||||
logStartup('Game WebSocket service initialized for /game namespace');
|
||||
|
||||
// Restore active games from snapshots (if any exist)
|
||||
gameWebSocketService.restoreAllActiveGames()
|
||||
.then(restoredCount => {
|
||||
if (restoredCount > 0) {
|
||||
logStartup(`Restored ${restoredCount} active game(s) from snapshots`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
logError('Failed to restore games from snapshots', error);
|
||||
});
|
||||
|
||||
// Start server with WebSocket support AFTER database is ready
|
||||
server = httpServer.listen(PORT, () => {
|
||||
logStartup('Server started successfully', {
|
||||
port: PORT,
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
timestamp: new Date().toISOString(),
|
||||
endpoints: {
|
||||
health: `/health`,
|
||||
swagger: `/api-docs`,
|
||||
users: `/api/users`,
|
||||
organizations: `/api/organizations`,
|
||||
decks: `/api/decks`,
|
||||
chats: `/api/chats`
|
||||
},
|
||||
websocket: {
|
||||
enabled: true,
|
||||
chatInactivityTimeout: `${process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'} minutes`
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
const dbOptions = AppDataSource.options as any;
|
||||
logConnection('Database connection failed', 'postgresql', 'failure', {
|
||||
error: error.message,
|
||||
type: dbOptions.type,
|
||||
host: dbOptions.host,
|
||||
database: dbOptions.database
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const gracefulShutdown = async (signal: string) => {
|
||||
logStartup(`Received ${signal}. Shutting down gracefully...`);
|
||||
|
||||
// Snapshot all active games before shutdown
|
||||
if (gameWebSocketService) {
|
||||
try {
|
||||
const snapshotCount = await gameWebSocketService.snapshotAllActiveGames();
|
||||
logStartup(`Created ${snapshotCount} game snapshot(s) before shutdown`);
|
||||
} catch (error) {
|
||||
logError('Failed to snapshot games before shutdown', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
server.close(() => {
|
||||
logStartup('HTTP server closed');
|
||||
|
||||
if (AppDataSource.isInitialized) {
|
||||
AppDataSource.destroy()
|
||||
.then(() => {
|
||||
logConnection('Database connection closed', 'postgresql', 'success');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
logError('Error during database shutdown', error);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
logError('Uncaught Exception - Server will shut down', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logError('Unhandled Rejection - Server will shut down', new Error(String(reason)), undefined, undefined);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Export WebSocket services for game integration
|
||||
export { webSocketService, gameWebSocketService };
|
||||
@@ -1,418 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import * as Minio from 'minio';
|
||||
|
||||
export enum LogLevel {
|
||||
REQUEST = 'REQUEST',
|
||||
ERROR = 'ERROR',
|
||||
WARNING = 'WARNING',
|
||||
AUTH = 'AUTH',
|
||||
DATABASE = 'DATABASE',
|
||||
STARTUP = 'STARTUP',
|
||||
CONNECTION = 'CONNECTION',
|
||||
OTHER = 'OTHER'
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
metadata?: any;
|
||||
requestId?: string;
|
||||
userId?: string;
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
method?: string;
|
||||
url?: string;
|
||||
statusCode?: number;
|
||||
responseTime?: number;
|
||||
}
|
||||
|
||||
export class LoggingService {
|
||||
private static instance: LoggingService;
|
||||
private minioClient: Minio.Client | null = null;
|
||||
private logBuffer: LogEntry[] = [];
|
||||
private currentLogFile: string | null = null;
|
||||
private logCount = 0;
|
||||
private readonly maxLogsPerFile = parseInt(process.env.MAX_LOGS_PER_FILE || '10000');
|
||||
private readonly logsDir = path.join(process.cwd(), 'logs');
|
||||
private readonly bucketName = process.env.MINIO_BUCKET_NAME || 'serpentrace-logs';
|
||||
private uploadInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.initializeLogsDirectory();
|
||||
this.initializeMinioClient();
|
||||
this.createNewLogFile();
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
this.startPeriodicUpload();
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => this.shutdown());
|
||||
process.on('SIGINT', () => this.shutdown());
|
||||
process.on('beforeExit', () => this.shutdown());
|
||||
}
|
||||
|
||||
static getInstance(): LoggingService {
|
||||
if (!LoggingService.instance) {
|
||||
LoggingService.instance = new LoggingService();
|
||||
}
|
||||
return LoggingService.instance;
|
||||
}
|
||||
|
||||
private initializeLogsDirectory(): void {
|
||||
try {
|
||||
if (!fs.existsSync(this.logsDir)) {
|
||||
fs.mkdirSync(this.logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create monthly subdirectory
|
||||
const monthlyDir = this.getMonthlyDirectory();
|
||||
if (!fs.existsSync(monthlyDir)) {
|
||||
fs.mkdirSync(monthlyDir, { recursive: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize logs directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private initializeMinioClient(): void {
|
||||
try {
|
||||
// Check if in production or development
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY) {
|
||||
this.minioClient = new Minio.Client({
|
||||
endPoint: process.env.MINIO_ENDPOINT,
|
||||
port: parseInt(process.env.MINIO_PORT || '9000'),
|
||||
useSSL: process.env.MINIO_USE_SSL === 'true',
|
||||
accessKey: process.env.MINIO_ACCESS_KEY,
|
||||
secretKey: process.env.MINIO_SECRET_KEY
|
||||
});
|
||||
|
||||
this.ensureBucketExists();
|
||||
} else {
|
||||
console.warn('Minio configuration not found. Logs will only be stored locally and in console.');
|
||||
}
|
||||
} else {
|
||||
// Development mode - only use MinIO if explicitly configured
|
||||
if (process.env.MINIO_ENDPOINT || process.env.ENABLE_MINIO === 'true') {
|
||||
this.minioClient = new Minio.Client({
|
||||
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
|
||||
port: parseInt(process.env.MINIO_PORT || '9000'),
|
||||
useSSL: false,
|
||||
accessKey: process.env.MINIO_ACCESS_KEY || 'serpentrace',
|
||||
secretKey: process.env.MINIO_SECRET_KEY || 'serpentrace123!'
|
||||
});
|
||||
|
||||
this.ensureBucketExists();
|
||||
} else {
|
||||
console.log('Development mode: MinIO disabled. Set ENABLE_MINIO=true to enable MinIO logging.');
|
||||
this.minioClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Minio client:', error);
|
||||
this.minioClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureBucketExists(): Promise<void> {
|
||||
if (!this.minioClient) return;
|
||||
|
||||
try {
|
||||
const exists = await this.minioClient.bucketExists(this.bucketName);
|
||||
if (!exists) {
|
||||
await this.minioClient.makeBucket(this.bucketName);
|
||||
this.log(LogLevel.STARTUP, `Created Minio bucket: ${this.bucketName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('MinIO connection failed - disabling MinIO logging:', (error as Error).message);
|
||||
// Disable MinIO client if connection fails
|
||||
this.minioClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private startPeriodicUpload(): void {
|
||||
// Upload current log file to Minio every 2 minutes
|
||||
this.uploadInterval = setInterval(async () => {
|
||||
if (this.currentLogFile && this.minioClient) {
|
||||
await this.uploadToMinio(this.currentLogFile);
|
||||
}
|
||||
}, 2 * 60 * 1000); // 2 minutes
|
||||
}
|
||||
|
||||
private getMonthlyDirectory(): string {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
return path.join(this.logsDir, `${year}-${month}`);
|
||||
}
|
||||
|
||||
private getMonthlyMinioPrefix(): string {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
return `${year}-${month}/`;
|
||||
}
|
||||
|
||||
private createNewLogFile(): void {
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().replace(/[:.]/g, '-');
|
||||
const fileName = `serpentrace-${timestamp}.log`;
|
||||
|
||||
this.currentLogFile = path.join(this.getMonthlyDirectory(), fileName);
|
||||
this.logCount = 0;
|
||||
|
||||
// Write log file header
|
||||
const header = `# SerpentRace Backend Logs\n# Started: ${now.toISOString()}\n# Max entries per file: ${this.maxLogsPerFile}\n\n`;
|
||||
try {
|
||||
fs.writeFileSync(this.currentLogFile, header);
|
||||
} catch (error) {
|
||||
console.error('Failed to create log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private formatLogEntry(entry: LogEntry): string {
|
||||
const parts = [
|
||||
entry.timestamp,
|
||||
`[${entry.level}]`,
|
||||
entry.message
|
||||
];
|
||||
|
||||
if (entry.requestId) parts.push(`ReqId:${entry.requestId}`);
|
||||
if (entry.userId) parts.push(`UserId:${entry.userId}`);
|
||||
if (entry.ip) parts.push(`IP:${entry.ip}`);
|
||||
if (entry.method && entry.url) parts.push(`${entry.method} ${entry.url}`);
|
||||
if (entry.statusCode) parts.push(`Status:${entry.statusCode}`);
|
||||
if (entry.responseTime) parts.push(`Time:${entry.responseTime}ms`);
|
||||
if (entry.userAgent) parts.push(`UA:${entry.userAgent.substring(0, 50)}`);
|
||||
if (entry.metadata) parts.push(`Meta:${JSON.stringify(entry.metadata)}`);
|
||||
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
private async writeToLocalFile(entry: LogEntry): Promise<void> {
|
||||
if (!this.currentLogFile) return;
|
||||
|
||||
try {
|
||||
const logLine = this.formatLogEntry(entry) + '\n';
|
||||
fs.appendFileSync(this.currentLogFile, logLine);
|
||||
|
||||
this.logCount++;
|
||||
|
||||
// Check if we need to rotate the log file
|
||||
if (this.logCount >= this.maxLogsPerFile) {
|
||||
await this.rotateLogFile();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to write to log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async rotateLogFile(): Promise<void> {
|
||||
if (!this.currentLogFile) return;
|
||||
|
||||
try {
|
||||
// Upload current file to Minio before rotating
|
||||
await this.uploadToMinio(this.currentLogFile);
|
||||
|
||||
// Create new log file
|
||||
this.createNewLogFile();
|
||||
|
||||
this.log(LogLevel.OTHER, 'Log file rotated due to size limit');
|
||||
} catch (error) {
|
||||
console.error('Failed to rotate log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadToMinio(filePath: string): Promise<void> {
|
||||
if (!this.minioClient) {
|
||||
console.warn('Minio client not initialized, skipping upload');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.warn(`Log file does not exist: ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileName = path.basename(filePath);
|
||||
const objectName = this.getMonthlyMinioPrefix() + fileName;
|
||||
|
||||
console.log(`Attempting to upload log file to Minio: ${objectName}`);
|
||||
await this.minioClient.fPutObject(this.bucketName, objectName, filePath);
|
||||
console.log(`Successfully uploaded log file to Minio: ${objectName}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to upload to Minio:', error);
|
||||
console.error('Minio config:', {
|
||||
endpoint: this.minioClient ? 'configured' : 'not configured',
|
||||
bucket: this.bucketName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private logToConsole(entry: LogEntry): void {
|
||||
// In production, skip OTHER, CONNECTION, and REQUEST logs
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (entry.level === LogLevel.OTHER ||
|
||||
entry.level === LogLevel.REQUEST) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const formattedEntry = this.formatLogEntry(entry);
|
||||
|
||||
switch (entry.level) {
|
||||
case LogLevel.ERROR:
|
||||
console.error(formattedEntry);
|
||||
break;
|
||||
case LogLevel.WARNING:
|
||||
console.warn(formattedEntry);
|
||||
break;
|
||||
case LogLevel.REQUEST:
|
||||
case LogLevel.AUTH:
|
||||
case LogLevel.DATABASE:
|
||||
case LogLevel.CONNECTION:
|
||||
console.info(formattedEntry);
|
||||
break;
|
||||
case LogLevel.STARTUP:
|
||||
console.log(formattedEntry);
|
||||
break;
|
||||
default:
|
||||
console.log(formattedEntry);
|
||||
}
|
||||
}
|
||||
|
||||
public log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
metadata?: any,
|
||||
req?: Request,
|
||||
res?: Response,
|
||||
responseTime?: number
|
||||
): void {
|
||||
// In production, skip OTHER, CONNECTION, and REQUEST logs entirely
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (level === LogLevel.OTHER ||
|
||||
level === LogLevel.CONNECTION ||
|
||||
level === LogLevel.REQUEST) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
metadata
|
||||
};
|
||||
|
||||
// Add request context if available
|
||||
if (req) {
|
||||
entry.requestId = (req as any).requestId || this.generateRequestId();
|
||||
entry.userId = (req as any).user?.userId;
|
||||
entry.ip = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||
entry.userAgent = req.get ? req.get('User-Agent') : 'unknown';
|
||||
entry.method = req.method;
|
||||
entry.url = req.originalUrl || req.url;
|
||||
}
|
||||
|
||||
if (res) {
|
||||
entry.statusCode = res.statusCode;
|
||||
}
|
||||
|
||||
if (responseTime !== undefined) {
|
||||
entry.responseTime = responseTime;
|
||||
}
|
||||
|
||||
// Log to all three destinations
|
||||
this.logToConsole(entry);
|
||||
this.writeToLocalFile(entry);
|
||||
|
||||
// Add to buffer for potential batch processing
|
||||
this.logBuffer.push(entry);
|
||||
|
||||
// Limit buffer size
|
||||
if (this.logBuffer.length > 1000) {
|
||||
this.logBuffer = this.logBuffer.slice(-500);
|
||||
}
|
||||
}
|
||||
|
||||
private generateRequestId(): string {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
try {
|
||||
// Clear the upload interval
|
||||
if (this.uploadInterval) {
|
||||
clearInterval(this.uploadInterval);
|
||||
this.uploadInterval = null;
|
||||
}
|
||||
|
||||
// Upload current log file to Minio
|
||||
if (this.currentLogFile) {
|
||||
await this.uploadToMinio(this.currentLogFile);
|
||||
}
|
||||
|
||||
this.log(LogLevel.STARTUP, 'Logging service shutting down gracefully');
|
||||
|
||||
// Give time for final logs to be written
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} catch (error) {
|
||||
console.error('Error during logging service shutdown:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware factory methods
|
||||
public requestLoggingMiddleware() {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Generate request ID
|
||||
(req as any).requestId = this.generateRequestId();
|
||||
|
||||
// Log request start
|
||||
this.log(LogLevel.REQUEST, `Incoming request`, undefined, req);
|
||||
|
||||
// Override res.end to log response
|
||||
const originalEnd = res.end.bind(res);
|
||||
res.end = (...args: any[]): Response => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
LoggingService.getInstance().log(
|
||||
LogLevel.REQUEST,
|
||||
`Request completed`,
|
||||
undefined,
|
||||
req,
|
||||
res,
|
||||
responseTime
|
||||
);
|
||||
return originalEnd(...args);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
public errorLoggingMiddleware() {
|
||||
return (error: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
this.log(
|
||||
LogLevel.ERROR,
|
||||
`Unhandled error: ${error.message}`,
|
||||
{
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
},
|
||||
req,
|
||||
res
|
||||
);
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default LoggingService;
|
||||
@@ -1,120 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
"experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -28,12 +28,12 @@ JWT_REFRESH_EXPIRATION=7d
|
||||
|
||||
# Email Configuration (SMTP)
|
||||
# CHANGE THESE: Configure your email provider
|
||||
EMAIL_HOST=smtp.yourmailprovider.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_SECURE=false
|
||||
EMAIL_USER=your_email@yourdomain.com
|
||||
EMAIL_PASS=your_email_password
|
||||
EMAIL_FROM="SerpentRace <noreply@yourdomain.com>"
|
||||
EMAIL_HOST=mail.serpentrace.hu
|
||||
EMAIL_PORT=465
|
||||
EMAIL_SECURE=true
|
||||
EMAIL_USER=noreply@serpentrace.hu
|
||||
EMAIL_PASS=ZUx720ece&Cin&F{
|
||||
EMAIL_FROM="SerpentRace <noreply@serpentrace.hu>"
|
||||
|
||||
# MinIO Object Storage
|
||||
MINIO_ENDPOINT=minio
|
||||
@@ -45,7 +45,8 @@ MINIO_SECRET_KEY=CHANGE_THIS_MINIO_SECRET_KEY_123!
|
||||
MINIO_BUCKET_NAME=serpentrace-logs
|
||||
|
||||
# Application Settings
|
||||
APP_BASE_URL=http://your-domain.com
|
||||
APP_BASE_URL=https://szesnake.ddc.sze.hu
|
||||
FRONTEND_URL=https://szesnake.ddc.sze.hu
|
||||
PORT=3000
|
||||
|
||||
# Chat System Limits
|
||||
|
||||
@@ -200,4 +200,12 @@ docker-compose -f docker-compose.deploy.yml up -d
|
||||
- PostgreSQL: 15-alpine
|
||||
- Redis: 7-alpine
|
||||
- MinIO: Latest
|
||||
- Nginx: Alpine
|
||||
- Nginx: Alpine
|
||||
|
||||
|
||||
## DataBase information
|
||||
- System: PostgreSQL
|
||||
- Server: postgres
|
||||
- Username: postgres
|
||||
- Password: postgres
|
||||
- Database: serpentrace
|
||||
@@ -3,36 +3,13 @@ version: '3.8'
|
||||
services:
|
||||
# Backend service using pre-built image
|
||||
backend:
|
||||
image: serpentrace_docker-backend:latest
|
||||
image: serpentrace-backend:latest
|
||||
container_name: serpentrace-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=serpentrace
|
||||
- DB_USERNAME=postgres
|
||||
- DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- JWT_EXPIRATION=${JWT_EXPIRATION:-24h}
|
||||
- JWT_REFRESH_EXPIRATION=${JWT_REFRESH_EXPIRATION:-7d}
|
||||
- MINIO_ENDPOINT=minio
|
||||
- MINIO_PORT=9000
|
||||
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||
- MINIO_USE_SSL=false
|
||||
- EMAIL_HOST=${EMAIL_HOST}
|
||||
- EMAIL_PORT=${EMAIL_PORT}
|
||||
- EMAIL_SECURE=${EMAIL_SECURE}
|
||||
- EMAIL_USER=${EMAIL_USER}
|
||||
- EMAIL_PASS=${EMAIL_PASS}
|
||||
- EMAIL_FROM=${EMAIL_FROM}
|
||||
env_file:
|
||||
- .env.server
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
depends_on:
|
||||
@@ -44,20 +21,15 @@ services:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- serpentrace-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
tty: true
|
||||
|
||||
# Frontend service using pre-built image
|
||||
frontend:
|
||||
image: serpentrace_docker-frontend:latest
|
||||
image: serpentrace-frontend:latest
|
||||
container_name: serpentrace-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
@@ -81,7 +53,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_DB: serpentrace
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
@@ -103,7 +75,7 @@ services:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
|
||||
command: redis-server --appendonly yes
|
||||
networks:
|
||||
- serpentrace-network
|
||||
healthcheck:
|
||||
@@ -121,8 +93,8 @@ services:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
|
||||
MINIO_ROOT_USER: serpentrace
|
||||
MINIO_ROOT_PASSWORD: serpentrace123!
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
@@ -134,6 +106,32 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Adminer Database Viewer
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: serpentrace-adminer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8083:8080" # Access via http://<server-ip>:8083
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- serpentrace-network
|
||||
|
||||
# Redis Commander
|
||||
redis-commander:
|
||||
image: rediscommander/redis-commander:latest
|
||||
container_name: serpentrace-redis-commander
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:8081" # Access via http://<server-ip>:8082
|
||||
environment:
|
||||
REDIS_HOSTS: local:serpentrace-redis:6379
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- serpentrace-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
|
||||
@@ -24,9 +24,9 @@ if %errorlevel% neq 0 (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if serpentRaceDocker.tar exists
|
||||
if not exist "serpentRaceDocker.tar" (
|
||||
echo [ERROR] serpentRaceDocker.tar not found!
|
||||
REM Check if serpentrace-images.tar exists
|
||||
if not exist "serpentrace-images.tar" (
|
||||
echo [ERROR] serpentrace-images.tar not found!
|
||||
echo Please ensure the tar file is in the same directory as this script.
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -40,8 +40,8 @@ if not exist ".env.server" (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Loading Docker images from serpentRaceDocker.tar...
|
||||
docker load -i serpentRaceDocker.tar
|
||||
echo [INFO] Loading Docker images from serpentrace-images.tar...
|
||||
docker load -i serpentrace-images.tar
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] Failed to load Docker images!
|
||||
pause
|
||||
|
||||
@@ -20,9 +20,9 @@ if ! command -v docker-compose &> /dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if serpentRaceDocker.tar exists
|
||||
if [ ! -f "serpentRaceDocker.tar" ]; then
|
||||
echo "[ERROR] serpentRaceDocker.tar not found!"
|
||||
# Check if serpentrace-images.tar exists
|
||||
if [ ! -f "serpentrace-images.tar" ]; then
|
||||
echo "[ERROR] serpentrace-images.tar not found!"
|
||||
echo "Please ensure the tar file is in the same directory as this script."
|
||||
exit 1
|
||||
fi
|
||||
@@ -34,8 +34,8 @@ if [ ! -f ".env.server" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[INFO] Loading Docker images from serpentRaceDocker.tar..."
|
||||
docker load -i serpentRaceDocker.tar
|
||||
echo "[INFO] Loading Docker images from serpentrace-images.tar..."
|
||||
docker load -i serpentrace-images.tar
|
||||
|
||||
echo "[INFO] Images loaded successfully!"
|
||||
echo
|
||||
|
||||
+38
-5
@@ -22,7 +22,7 @@ server {
|
||||
|
||||
# API proxy to backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3000/;
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
@@ -45,10 +45,43 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Static assets caching
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
# Adminer Database Viewer proxy
|
||||
location /adminer/ {
|
||||
proxy_pass http://adminer:8080/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Redis Commander proxy
|
||||
location /redis/ {
|
||||
proxy_pass http://redis-commander:8081/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect http://redis-commander:8081/ /redis/;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# MinIO Console proxy
|
||||
location /minio/ {
|
||||
proxy_pass http://minio:9001/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect http://minio:9001/ /minio/;
|
||||
sub_filter '<base href="/"' '<base href="/minio/"';
|
||||
sub_filter_types text/html;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"Port": 5432,
|
||||
"MaintenanceDB": "serpentrace",
|
||||
"Username": "postgres",
|
||||
"Password": "postgres",
|
||||
"SSLMode": "prefer",
|
||||
"Comment": "SerpentRace Production Database"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+180
-236
@@ -1,236 +1,180 @@
|
||||
-- SerpentRace Database Schema
|
||||
-- Generated from TypeORM Entity Aggregates
|
||||
-- This file creates the complete database schema without initial data
|
||||
|
||||
-- Enable UUID extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Create Users table
|
||||
CREATE TABLE "Users" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"orgid" UUID NULL,
|
||||
"username" VARCHAR(100) UNIQUE NOT NULL,
|
||||
"password" VARCHAR(255) NOT NULL,
|
||||
"email" VARCHAR(255) UNIQUE NOT NULL,
|
||||
"fname" VARCHAR(100) NOT NULL,
|
||||
"lname" VARCHAR(100) NOT NULL,
|
||||
"token" VARCHAR(255) NULL,
|
||||
"TokenExpires" TIMESTAMP NULL,
|
||||
"phone" VARCHAR(20) NULL,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"Orglogindate" TIMESTAMP NULL
|
||||
);
|
||||
|
||||
-- Create Organizations table
|
||||
CREATE TABLE "Organizations" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"contactfname" VARCHAR(100) NOT NULL,
|
||||
"contactlname" VARCHAR(100) NOT NULL,
|
||||
"contactphone" VARCHAR(20) NOT NULL,
|
||||
"contactemail" VARCHAR(255) NOT NULL,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"url" VARCHAR(500) NULL,
|
||||
"userinorg" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxOrganizationalDecks" INTEGER NULL
|
||||
);
|
||||
|
||||
-- Create Decks table
|
||||
CREATE TABLE "Decks" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"type" INTEGER NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"creation_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cards" JSONB NOT NULL DEFAULT '[]',
|
||||
"played_number" INTEGER NOT NULL DEFAULT 0,
|
||||
"ctype" INTEGER NOT NULL DEFAULT 0,
|
||||
"update_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"organization_id" UUID NULL
|
||||
);
|
||||
|
||||
-- Create Chats table
|
||||
CREATE TABLE "Chats" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"type" VARCHAR(50) NOT NULL DEFAULT 'direct',
|
||||
"name" VARCHAR(255) NULL,
|
||||
"gameId" UUID NULL,
|
||||
"createdBy" UUID NULL,
|
||||
"users" UUID[] NOT NULL,
|
||||
"messages" JSONB NOT NULL DEFAULT '[]',
|
||||
"lastActivity" TIMESTAMP NULL,
|
||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"archiveDate" TIMESTAMP NULL
|
||||
);
|
||||
|
||||
-- Create Contacts table
|
||||
CREATE TABLE "Contacts" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"email" VARCHAR(255) NOT NULL,
|
||||
"userid" UUID NULL,
|
||||
"type" INTEGER NOT NULL,
|
||||
"txt" TEXT NOT NULL,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"adminResponse" TEXT NULL,
|
||||
"responseDate" TIMESTAMP NULL,
|
||||
"respondedBy" UUID NULL
|
||||
);
|
||||
|
||||
-- Create Games table
|
||||
CREATE TABLE "Games" (
|
||||
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
"gamecode" VARCHAR(10) UNIQUE NOT NULL,
|
||||
"maxplayers" INTEGER NOT NULL,
|
||||
"logintype" INTEGER NOT NULL DEFAULT 0,
|
||||
"state" INTEGER NOT NULL DEFAULT 0,
|
||||
"playerids" UUID[] NOT NULL DEFAULT '{}',
|
||||
"decks" JSONB NOT NULL DEFAULT '[]',
|
||||
"boardsize" INTEGER NOT NULL DEFAULT 50,
|
||||
"createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"finishDate" TIMESTAMP NULL,
|
||||
"winnerid" UUID NULL,
|
||||
"createdBy" UUID NOT NULL,
|
||||
"organizationid" UUID NULL
|
||||
);
|
||||
|
||||
-- Add Foreign Key Constraints
|
||||
ALTER TABLE "Users"
|
||||
ADD CONSTRAINT "FK_Users_Organizations"
|
||||
FOREIGN KEY ("orgid") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Decks"
|
||||
ADD CONSTRAINT "FK_Decks_Users"
|
||||
FOREIGN KEY ("user_id") REFERENCES "Users"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "Decks"
|
||||
ADD CONSTRAINT "FK_Decks_Organizations"
|
||||
FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Contacts"
|
||||
ADD CONSTRAINT "FK_Contacts_Users"
|
||||
FOREIGN KEY ("userid") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Contacts"
|
||||
ADD CONSTRAINT "FK_Contacts_RespondedBy"
|
||||
FOREIGN KEY ("respondedBy") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Chats"
|
||||
ADD CONSTRAINT "FK_Chats_CreatedBy"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Chats"
|
||||
ADD CONSTRAINT "FK_Chats_Games"
|
||||
FOREIGN KEY ("gameId") REFERENCES "Games"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Games"
|
||||
ADD CONSTRAINT "FK_Games_CreatedBy"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "Games"
|
||||
ADD CONSTRAINT "FK_Games_Organizations"
|
||||
FOREIGN KEY ("organizationid") REFERENCES "Organizations"("id") ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE "Games"
|
||||
ADD CONSTRAINT "FK_Games_Winner"
|
||||
FOREIGN KEY ("winnerid") REFERENCES "Users"("id") ON DELETE SET NULL;
|
||||
|
||||
-- Create Indexes for Performance
|
||||
CREATE INDEX "IDX_Users_Username" ON "Users" ("username");
|
||||
CREATE INDEX "IDX_Users_Email" ON "Users" ("email");
|
||||
CREATE INDEX "IDX_Users_OrgId" ON "Users" ("orgid");
|
||||
CREATE INDEX "IDX_Users_State" ON "Users" ("state");
|
||||
|
||||
CREATE INDEX "IDX_Organizations_Name" ON "Organizations" ("name");
|
||||
CREATE INDEX "IDX_Organizations_State" ON "Organizations" ("state");
|
||||
|
||||
CREATE INDEX "IDX_Decks_UserId" ON "Decks" ("user_id");
|
||||
CREATE INDEX "IDX_Decks_Type" ON "Decks" ("type");
|
||||
CREATE INDEX "IDX_Decks_CType" ON "Decks" ("ctype");
|
||||
CREATE INDEX "IDX_Decks_State" ON "Decks" ("state");
|
||||
CREATE INDEX "IDX_Decks_OrganizationId" ON "Decks" ("organization_id");
|
||||
|
||||
CREATE INDEX "IDX_Chats_Type" ON "Chats" ("type");
|
||||
CREATE INDEX "IDX_Chats_State" ON "Chats" ("state");
|
||||
CREATE INDEX "IDX_Chats_GameId" ON "Chats" ("gameId");
|
||||
CREATE INDEX "IDX_Chats_CreatedBy" ON "Chats" ("createdBy");
|
||||
|
||||
CREATE INDEX "IDX_Contacts_Type" ON "Contacts" ("type");
|
||||
CREATE INDEX "IDX_Contacts_State" ON "Contacts" ("state");
|
||||
CREATE INDEX "IDX_Contacts_UserId" ON "Contacts" ("userid");
|
||||
|
||||
CREATE INDEX "IDX_Games_GameCode" ON "Games" ("gamecode");
|
||||
CREATE INDEX "IDX_Games_State" ON "Games" ("state");
|
||||
CREATE INDEX "IDX_Games_CreatedBy" ON "Games" ("createdBy");
|
||||
CREATE INDEX "IDX_Games_OrganizationId" ON "Games" ("organizationid");
|
||||
|
||||
-- Create update trigger for updatedate columns
|
||||
CREATE OR REPLACE FUNCTION update_updatedate_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updatedate = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Apply update triggers
|
||||
CREATE TRIGGER update_users_updatedate
|
||||
BEFORE UPDATE ON "Users"
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
|
||||
|
||||
CREATE TRIGGER update_organizations_updatedate
|
||||
BEFORE UPDATE ON "Organizations"
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
|
||||
|
||||
CREATE TRIGGER update_decks_updatedate
|
||||
BEFORE UPDATE ON "Decks"
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
|
||||
|
||||
CREATE TRIGGER update_chats_updatedate
|
||||
BEFORE UPDATE ON "Chats"
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
|
||||
|
||||
CREATE TRIGGER update_contacts_updatedate
|
||||
BEFORE UPDATE ON "Contacts"
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
|
||||
|
||||
CREATE TRIGGER update_games_updatedate
|
||||
BEFORE UPDATE ON "Games"
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
|
||||
|
||||
-- Comments for documentation
|
||||
COMMENT ON TABLE "Users" IS 'User accounts with authentication and profile information';
|
||||
COMMENT ON TABLE "Organizations" IS 'Organizations that can have multiple users and premium features';
|
||||
COMMENT ON TABLE "Decks" IS 'Card decks for the game, can be public, private, or organizational';
|
||||
COMMENT ON TABLE "Chats" IS 'Chat system supporting direct messages, groups, and game chats';
|
||||
COMMENT ON TABLE "Contacts" IS 'Contact form submissions and support tickets';
|
||||
COMMENT ON TABLE "Games" IS 'Game sessions with players, decks, and game state';
|
||||
|
||||
-- Enum value comments
|
||||
COMMENT ON COLUMN "Users"."state" IS '0=REGISTERED_NOT_VERIFIED, 1=VERIFIED_REGULAR, 2=VERIFIED_PREMIUM, 3=SOFT_DELETE, 4=DEACTIVATED, 5=ADMIN';
|
||||
COMMENT ON COLUMN "Organizations"."state" IS '0=REGISTERED, 1=ACTIVE, 2=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Decks"."type" IS '0=LUCK, 1=JOKER, 2=QUESTION';
|
||||
COMMENT ON COLUMN "Decks"."ctype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION';
|
||||
COMMENT ON COLUMN "Decks"."state" IS '0=ACTIVE, 1=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Chats"."type" IS 'direct, group, game';
|
||||
COMMENT ON COLUMN "Chats"."state" IS '0=ACTIVE, 1=ARCHIVE, 2=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Contacts"."type" IS '0=BUG, 1=PROBLEM, 2=QUESTION, 3=SALES, 4=OTHER';
|
||||
COMMENT ON COLUMN "Contacts"."state" IS '0=ACTIVE, 1=RESOLVED, 2=SOFT_DELETE';
|
||||
COMMENT ON COLUMN "Games"."state" IS '0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED';
|
||||
COMMENT ON COLUMN "Games"."logintype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION';
|
||||
|
||||
-- Grant permissions for application user
|
||||
-- Note: Replace 'serpentrace_app' with your actual application database user
|
||||
-- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO serpentrace_app;
|
||||
-- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO serpentrace_app;
|
||||
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO serpentrace_app;
|
||||
-- This script was generated by the ERD tool in pgAdmin 4.
|
||||
-- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps.
|
||||
BEGIN;
|
||||
|
||||
-- ===================================================================
|
||||
-- STEP 1: Enable Required Extensions
|
||||
-- ===================================================================
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- ===================================================================
|
||||
-- STEP 2: Create Tables
|
||||
-- ===================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public."ChatArchives"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"chatId" uuid NOT NULL,
|
||||
"archivedMessages" json NOT NULL,
|
||||
"archivedAt" timestamp without time zone NOT NULL,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL,
|
||||
"chatName" character varying(255) COLLATE pg_catalog."default",
|
||||
"gameId" uuid,
|
||||
participants uuid[] NOT NULL,
|
||||
CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public."Chats"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying,
|
||||
name character varying(255) COLLATE pg_catalog."default",
|
||||
"gameId" uuid,
|
||||
"createdBy" uuid,
|
||||
users uuid[] NOT NULL,
|
||||
messages json NOT NULL DEFAULT '[]'::json,
|
||||
"lastActivity" timestamp without time zone,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
"archiveDate" timestamp without time zone,
|
||||
CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public."Contacts"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
userid uuid,
|
||||
type integer NOT NULL,
|
||||
txt text COLLATE pg_catalog."default" NOT NULL,
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"adminResponse" text COLLATE pg_catalog."default",
|
||||
"responseDate" timestamp without time zone,
|
||||
"respondedBy" uuid,
|
||||
CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public."Decks"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
type integer NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
creation_date timestamp without time zone NOT NULL DEFAULT now(),
|
||||
cards json NOT NULL,
|
||||
played_number integer NOT NULL DEFAULT 0,
|
||||
ctype integer NOT NULL DEFAULT 0,
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
organization_id uuid,
|
||||
CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public."Games"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL,
|
||||
maxplayers integer NOT NULL,
|
||||
logintype integer NOT NULL DEFAULT 0,
|
||||
boardsize integer NOT NULL DEFAULT 50,
|
||||
"createdBy" uuid NOT NULL,
|
||||
organizationid uuid,
|
||||
decks jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
playerids uuid[] NOT NULL DEFAULT '{}'::uuid[],
|
||||
"winnerId" uuid,
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
"createDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
start_date timestamp without time zone,
|
||||
"finishDate" timestamp without time zone,
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"organizationId" uuid,
|
||||
CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id),
|
||||
CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public."Organizations"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL,
|
||||
contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
regdate timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
url character varying(500) COLLATE pg_catalog."default",
|
||||
userinorg integer NOT NULL DEFAULT 0,
|
||||
"maxOrganizationalDecks" integer,
|
||||
CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public."Users"
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
orgid uuid,
|
||||
username character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
password character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
fname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
lname character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
token character varying(255) COLLATE pg_catalog."default",
|
||||
"TokenExpires" timestamp without time zone,
|
||||
phone character varying(20) COLLATE pg_catalog."default",
|
||||
state integer NOT NULL DEFAULT 0,
|
||||
regdate timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"updateDate" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"Orglogindate" timestamp without time zone,
|
||||
CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id),
|
||||
CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email),
|
||||
CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.migrations
|
||||
(
|
||||
id serial NOT NULL,
|
||||
"timestamp" bigint NOT NULL,
|
||||
name character varying COLLATE pg_catalog."default" NOT NULL,
|
||||
CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
ALTER TABLE IF EXISTS public."Decks"
|
||||
ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id)
|
||||
REFERENCES public."Organizations" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
|
||||
ALTER TABLE IF EXISTS public."Decks"
|
||||
ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id)
|
||||
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
|
||||
ALTER TABLE IF EXISTS public."Games"
|
||||
ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId")
|
||||
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
|
||||
ALTER TABLE IF EXISTS public."Games"
|
||||
ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId")
|
||||
REFERENCES public."Organizations" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
|
||||
ALTER TABLE IF EXISTS public."Games"
|
||||
ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy")
|
||||
REFERENCES public."Users" (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
END;
|
||||
Reference in New Issue
Block a user