security improvements
This commit is contained in:
67
.env.example
Normal file
67
.env.example
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# ACC Server Manager Environment Configuration
|
||||||
|
# Copy this file to .env and update with your actual values
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CRITICAL SECURITY SETTINGS (REQUIRED)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# JWT Secret Key - MUST be changed in production
|
||||||
|
# Generate with: openssl rand -base64 64
|
||||||
|
JWT_SECRET=your-super-secure-jwt-secret-key-minimum-32-chars-long-change-this-in-production
|
||||||
|
|
||||||
|
# Application Secrets - MUST be changed in production
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
APP_SECRET=your-super-secure-app-secret-change-this-in-production
|
||||||
|
APP_SECRET_CODE=your-super-secure-app-secret-code-change-this-in-production
|
||||||
|
|
||||||
|
# Encryption Key for sensitive data (MUST be exactly 32 characters for AES-256)
|
||||||
|
# Generate with: openssl rand -hex 16
|
||||||
|
ENCRYPTION_KEY=your-32-character-encryption-key-here
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CORE APPLICATION SETTINGS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Database file name (SQLite)
|
||||||
|
DB_NAME=acc.db
|
||||||
|
|
||||||
|
# Server port
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# CORS allowed origin (use specific domains in production)
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:5173
|
||||||
|
|
||||||
|
# Default admin password for initial setup (change after first login)
|
||||||
|
PASSWORD=change-this-default-admin-password
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# INSTRUCTIONS FOR PRODUCTION DEPLOYMENT
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Generate secure secrets:
|
||||||
|
# - JWT_SECRET: openssl rand -base64 64
|
||||||
|
# - APP_SECRET: openssl rand -hex 32
|
||||||
|
# - APP_SECRET_CODE: openssl rand -hex 32
|
||||||
|
# - ENCRYPTION_KEY: openssl rand -hex 16
|
||||||
|
|
||||||
|
# 2. Set appropriate CORS origins for your domain
|
||||||
|
|
||||||
|
# 3. Change the default PASSWORD immediately after first login
|
||||||
|
|
||||||
|
# 4. NEVER commit actual secrets to version control!
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# OPTIONAL SETTINGS (These are handled by system config in database)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# The following settings are managed through the application's system config
|
||||||
|
# and stored in the database. They are listed here for reference only:
|
||||||
|
#
|
||||||
|
# - SteamCMD path (configured via web interface)
|
||||||
|
# - NSSM path (configured via web interface)
|
||||||
|
# - Logging settings (handled by application defaults)
|
||||||
|
# - Rate limiting (handled by application defaults)
|
||||||
|
# - Backup settings (handled by application defaults)
|
||||||
|
# - Monitoring settings (handled by application defaults)
|
||||||
|
#
|
||||||
|
# These can be configured through the web interface after installation.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,3 +34,4 @@ _testmain.go
|
|||||||
|
|
||||||
# .Dockerfile
|
# .Dockerfile
|
||||||
|
|
||||||
|
.zed
|
||||||
|
|||||||
406
README.md
Normal file
406
README.md
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
# ACC Server Manager
|
||||||
|
|
||||||
|
A comprehensive web-based management system for Assetto Corsa Competizione (ACC) dedicated servers. This application provides a modern, secure interface for managing multiple ACC server instances with advanced features like automated Steam integration, firewall management, and real-time monitoring.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
### Core Server Management
|
||||||
|
- **Multi-Server Support**: Manage multiple ACC server instances from a single interface
|
||||||
|
- **Configuration Management**: Web-based configuration editor with validation
|
||||||
|
- **Service Integration**: Windows Service management via NSSM
|
||||||
|
- **Port Management**: Automatic port allocation and firewall rule creation
|
||||||
|
- **Real-time Monitoring**: Live server status and performance metrics
|
||||||
|
|
||||||
|
### Steam Integration
|
||||||
|
- **Automated Installation**: Automatic ACC server installation via SteamCMD
|
||||||
|
- **Credential Management**: Secure Steam credential storage with AES-256 encryption
|
||||||
|
- **Update Management**: Automated server updates and maintenance
|
||||||
|
|
||||||
|
### Security Features
|
||||||
|
- **JWT Authentication**: Secure token-based authentication system
|
||||||
|
- **Role-Based Access**: Granular permission system with user roles
|
||||||
|
- **Rate Limiting**: Protection against brute force and DoS attacks
|
||||||
|
- **Input Validation**: Comprehensive input sanitization and validation
|
||||||
|
- **Security Headers**: OWASP-compliant security headers
|
||||||
|
- **Password Security**: Bcrypt password hashing with strength validation
|
||||||
|
|
||||||
|
### Monitoring & Analytics
|
||||||
|
- **State History**: Track server state changes and player activity
|
||||||
|
- **Performance Metrics**: Server performance and usage statistics
|
||||||
|
- **Activity Logs**: Comprehensive logging and audit trails
|
||||||
|
- **Dashboard**: Real-time overview of all managed servers
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Backend**: Go 1.23.0 with Fiber web framework
|
||||||
|
- **Database**: SQLite with GORM ORM
|
||||||
|
- **Authentication**: JWT tokens with bcrypt password hashing
|
||||||
|
- **API Documentation**: Swagger/OpenAPI integration
|
||||||
|
- **Dependency Injection**: Uber Dig container
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
acc-server-manager/
|
||||||
|
├── cmd/
|
||||||
|
│ └── api/ # Application entry point
|
||||||
|
├── local/
|
||||||
|
│ ├── api/ # API route definitions
|
||||||
|
│ ├── controller/ # HTTP request handlers
|
||||||
|
│ ├── middleware/ # Authentication and security middleware
|
||||||
|
│ ├── model/ # Database models and business logic
|
||||||
|
│ ├── repository/ # Data access layer
|
||||||
|
│ ├── service/ # Business logic services
|
||||||
|
│ └── utl/ # Utilities and shared components
|
||||||
|
│ ├── cache/ # Caching utilities
|
||||||
|
│ ├── command/ # Command execution utilities
|
||||||
|
│ ├── common/ # Common utilities
|
||||||
|
│ ├── configs/ # Configuration management
|
||||||
|
│ ├── db/ # Database connection and migration
|
||||||
|
│ ├── jwt/ # JWT token management
|
||||||
|
│ ├── logging/ # Logging utilities
|
||||||
|
│ ├── network/ # Network utilities
|
||||||
|
│ ├── password/ # Password hashing utilities
|
||||||
|
│ ├── regex_handler/ # Regular expression utilities
|
||||||
|
│ ├── server/ # HTTP server configuration
|
||||||
|
│ └── tracking/ # Server state tracking
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── logs/ # Application logs
|
||||||
|
└── vendor/ # Go dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
### System Requirements
|
||||||
|
- **Operating System**: Windows 10/11 or Windows Server 2016+
|
||||||
|
- **Go**: Version 1.23.0 or later
|
||||||
|
- **SteamCMD**: For ACC server installation and updates
|
||||||
|
- **NSSM**: Non-Sucking Service Manager for Windows services
|
||||||
|
- **PowerShell**: Version 5.0 or later
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- ACC Dedicated Server files
|
||||||
|
- Valid Steam account (for server installation)
|
||||||
|
- Administrative privileges (for service and firewall management)
|
||||||
|
|
||||||
|
## ⚙️ Installation
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd acc-server-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependencies
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Generate Environment Configuration
|
||||||
|
We provide scripts to automatically generate secure secrets and create your `.env` file:
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
.\scripts\generate-secrets.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/macOS (Bash):**
|
||||||
|
```bash
|
||||||
|
./scripts/generate-secrets.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Setup:**
|
||||||
|
If you prefer to set up manually:
|
||||||
|
```bash
|
||||||
|
copy .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Then generate secure secrets:
|
||||||
|
```bash
|
||||||
|
# JWT Secret (64 bytes, base64 encoded)
|
||||||
|
openssl rand -base64 64
|
||||||
|
|
||||||
|
# Application secrets (32 bytes, hex encoded)
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Encryption key (16 bytes, hex encoded = 32 characters)
|
||||||
|
openssl rand -hex 16
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with your generated secrets:
|
||||||
|
```env
|
||||||
|
# Security Settings (REQUIRED)
|
||||||
|
JWT_SECRET=your-generated-jwt-secret-here
|
||||||
|
APP_SECRET=your-generated-app-secret-here
|
||||||
|
APP_SECRET_CODE=your-generated-secret-code-here
|
||||||
|
ENCRYPTION_KEY=your-generated-32-character-hex-key
|
||||||
|
|
||||||
|
# Core Application Settings
|
||||||
|
PORT=3000
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:5173
|
||||||
|
DB_NAME=acc.db
|
||||||
|
PASSWORD=change-this-default-admin-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Build the Application
|
||||||
|
```bash
|
||||||
|
go build -o api.exe cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Run the Application
|
||||||
|
```bash
|
||||||
|
./api.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3000`
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
The application uses minimal environment variables, with most settings managed through the web interface:
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `JWT_SECRET` | Yes | - | JWT signing secret (64+ chars, base64) |
|
||||||
|
| `APP_SECRET` | Yes | - | Application secret key (32 bytes, hex) |
|
||||||
|
| `APP_SECRET_CODE` | Yes | - | Application secret code (32 bytes, hex) |
|
||||||
|
| `ENCRYPTION_KEY` | Yes | - | AES-256 encryption key (32 hex chars) |
|
||||||
|
| `PORT` | No | 3000 | HTTP server port |
|
||||||
|
| `DB_NAME` | No | acc.db | SQLite database filename |
|
||||||
|
| `CORS_ALLOWED_ORIGIN` | No | http://localhost:5173 | CORS allowed origin |
|
||||||
|
| `PASSWORD` | No | - | Default admin password for initial setup |
|
||||||
|
|
||||||
|
**⚠️ Important**: All required secrets are automatically generated by the provided scripts in `scripts/` directory.
|
||||||
|
|
||||||
|
### System Configuration (Web Interface)
|
||||||
|
|
||||||
|
Advanced settings are managed through the web interface and stored in the database:
|
||||||
|
- **Steam Integration**: SteamCMD path and credentials
|
||||||
|
- **Service Management**: NSSM path and service settings
|
||||||
|
- **Server Settings**: Default ports, firewall rules
|
||||||
|
- **Security Policies**: Rate limits, session timeouts
|
||||||
|
- **Monitoring**: Logging levels, performance tracking
|
||||||
|
- **Backup Settings**: Automatic backup configuration
|
||||||
|
|
||||||
|
Access these settings through the admin panel after initial setup.
|
||||||
|
|
||||||
|
## 🔒 Security
|
||||||
|
|
||||||
|
This application implements comprehensive security measures:
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
- **JWT Tokens**: Secure token-based authentication
|
||||||
|
- **Password Security**: Bcrypt hashing with strength validation
|
||||||
|
- **Role-Based Access**: Granular permission system
|
||||||
|
- **Session Management**: Configurable timeouts and lockouts
|
||||||
|
|
||||||
|
### Protection Mechanisms
|
||||||
|
- **Rate Limiting**: Multiple layers of rate limiting
|
||||||
|
- **Input Validation**: Comprehensive input sanitization
|
||||||
|
- **Security Headers**: OWASP-compliant HTTP headers
|
||||||
|
- **CORS Protection**: Configurable cross-origin restrictions
|
||||||
|
- **Request Limits**: Size and timeout limitations
|
||||||
|
|
||||||
|
### Monitoring & Logging
|
||||||
|
- **Security Events**: Authentication and authorization logging
|
||||||
|
- **Audit Trail**: Comprehensive activity logging
|
||||||
|
- **Threat Detection**: Suspicious activity monitoring
|
||||||
|
|
||||||
|
For detailed security information, see [SECURITY.md](docs/SECURITY.md).
|
||||||
|
|
||||||
|
## 📚 API Documentation
|
||||||
|
|
||||||
|
The application includes comprehensive API documentation via Swagger UI:
|
||||||
|
- **Local Development**: http://localhost:3000/swagger/
|
||||||
|
- **Interactive Testing**: Test API endpoints directly from the browser
|
||||||
|
- **Schema Documentation**: Complete request/response schemas
|
||||||
|
|
||||||
|
### Key API Endpoints
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
- `POST /api/v1/auth/login` - User authentication
|
||||||
|
- `POST /api/v1/auth/register` - User registration
|
||||||
|
- `GET /api/v1/auth/me` - Get current user
|
||||||
|
|
||||||
|
#### Server Management
|
||||||
|
- `GET /api/v1/servers` - List all servers
|
||||||
|
- `POST /api/v1/servers` - Create new server
|
||||||
|
- `GET /api/v1/servers/{id}` - Get server details
|
||||||
|
- `PUT /api/v1/servers/{id}` - Update server
|
||||||
|
- `DELETE /api/v1/servers/{id}` - Delete server
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
- `GET /api/v1/servers/{id}/config/{file}` - Get configuration file
|
||||||
|
- `PUT /api/v1/servers/{id}/config/{file}` - Update configuration
|
||||||
|
- `POST /api/v1/servers/{id}/restart` - Restart server
|
||||||
|
|
||||||
|
## 🖥️ Frontend Integration
|
||||||
|
|
||||||
|
This backend is designed to work with a modern web frontend. Recommended stack:
|
||||||
|
- **React/Vue/Angular**: Modern JavaScript framework
|
||||||
|
- **TypeScript**: Type safety and better development experience
|
||||||
|
- **Axios/Fetch**: HTTP client for API communication
|
||||||
|
- **WebSocket**: Real-time server status updates
|
||||||
|
|
||||||
|
### CORS Configuration
|
||||||
|
Configure `CORS_ALLOWED_ORIGIN` to match your frontend URL:
|
||||||
|
```env
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:3000,https://yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
### Running in Development Mode
|
||||||
|
```bash
|
||||||
|
# Install air for hot reloading (optional)
|
||||||
|
go install github.com/cosmtrek/air@latest
|
||||||
|
|
||||||
|
# Run with hot reload
|
||||||
|
air
|
||||||
|
|
||||||
|
# Or run directly with go
|
||||||
|
go run cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
```bash
|
||||||
|
# View database schema
|
||||||
|
sqlite3 acc.db ".schema"
|
||||||
|
|
||||||
|
# Backup database
|
||||||
|
copy acc.db acc_backup.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Run specific test package
|
||||||
|
go test ./local/service/...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Production Deployment
|
||||||
|
|
||||||
|
### 1. Generate Production Secrets
|
||||||
|
```bash
|
||||||
|
# Use the secret generation script for production
|
||||||
|
.\scripts\generate-secrets.ps1 # Windows
|
||||||
|
./scripts/generate-secrets.sh # Linux/macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build for Production
|
||||||
|
```bash
|
||||||
|
# Build optimized binary
|
||||||
|
go build -ldflags="-w -s" -o acc-server-manager.exe cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Security Checklist
|
||||||
|
- [ ] Generate unique production secrets (use provided scripts)
|
||||||
|
- [ ] Configure production CORS origins in `.env`
|
||||||
|
- [ ] Change default admin password immediately after first login
|
||||||
|
- [ ] Enable HTTPS with valid certificates
|
||||||
|
- [ ] Set up proper firewall rules
|
||||||
|
- [ ] Configure system paths via web interface
|
||||||
|
- [ ] Set up monitoring and alerting
|
||||||
|
- [ ] Test all security configurations
|
||||||
|
|
||||||
|
### 3. Service Installation
|
||||||
|
```bash
|
||||||
|
# Create Windows service using NSSM
|
||||||
|
nssm install "ACC Server Manager" "C:\path\to\acc-server-manager.exe"
|
||||||
|
nssm set "ACC Server Manager" DisplayName "ACC Server Manager"
|
||||||
|
nssm set "ACC Server Manager" Description "Assetto Corsa Competizione Server Manager"
|
||||||
|
nssm start "ACC Server Manager"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Monitoring Setup
|
||||||
|
- Configure log rotation
|
||||||
|
- Set up health check monitoring
|
||||||
|
- Configure alerting for critical errors
|
||||||
|
- Monitor resource usage and performance
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### "JWT_SECRET environment variable is required"
|
||||||
|
**Solution**: Set the JWT_SECRET environment variable with a secure 32+ character string.
|
||||||
|
|
||||||
|
#### "Failed to connect database"
|
||||||
|
**Solution**: Ensure the application has write permissions to the database directory.
|
||||||
|
|
||||||
|
#### "SteamCMD not found"
|
||||||
|
**Solution**: Install SteamCMD and update the `STEAMCMD_PATH` environment variable.
|
||||||
|
|
||||||
|
#### "Permission denied creating firewall rule"
|
||||||
|
**Solution**: Run the application as Administrator for firewall management.
|
||||||
|
|
||||||
|
### Log Locations
|
||||||
|
- **Application Logs**: `./logs/app.log`
|
||||||
|
- **Error Logs**: `./logs/error.log`
|
||||||
|
- **Security Logs**: `./logs/security.log`
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
Enable debug logging:
|
||||||
|
```env
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
DEBUG_MODE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||||
|
3. Make your changes and add tests
|
||||||
|
4. Ensure all tests pass: `go test ./...`
|
||||||
|
5. Commit your changes: `git commit -m 'Add amazing feature'`
|
||||||
|
6. Push to the branch: `git push origin feature/amazing-feature`
|
||||||
|
7. Open a Pull Request
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- Follow Go best practices and conventions
|
||||||
|
- Use `gofmt` for code formatting
|
||||||
|
- Add comprehensive comments for public functions
|
||||||
|
- Include tests for new functionality
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- Never commit secrets or credentials
|
||||||
|
- Follow secure coding practices
|
||||||
|
- Test security features thoroughly
|
||||||
|
- Report security issues privately
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- **Fiber Framework**: High-performance HTTP framework
|
||||||
|
- **GORM**: Powerful ORM for Go
|
||||||
|
- **Assetto Corsa Competizione**: The amazing racing simulation
|
||||||
|
- **Community**: Contributors and users who make this project possible
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [Security Guide](docs/SECURITY.md)
|
||||||
|
- [API Documentation](http://localhost:3000/swagger/)
|
||||||
|
- [Configuration Guide](docs/CONFIGURATION.md)
|
||||||
|
|
||||||
|
### Community
|
||||||
|
- **Issues**: Report bugs and request features via GitHub Issues
|
||||||
|
- **Discussions**: Join community discussions
|
||||||
|
- **Wiki**: Community-maintained documentation and guides
|
||||||
|
|
||||||
|
### Professional Support
|
||||||
|
For professional support, consulting, or custom development, please contact the maintainers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy Racing! 🏁**
|
||||||
@@ -8,14 +8,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"go.uber.org/dig"
|
"go.uber.org/dig"
|
||||||
|
|
||||||
_ "acc-server-manager/docs"
|
_ "acc-server-manager/docs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
godotenv.Load()
|
|
||||||
// Initialize logger
|
// Initialize logger
|
||||||
logger, err := logging.Initialize()
|
logger, err := logging.Initialize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
822
documentation/API.md
Normal file
822
documentation/API.md
Normal file
@@ -0,0 +1,822 @@
|
|||||||
|
# API Documentation for ACC Server Manager
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The ACC Server Manager provides a comprehensive REST API for managing Assetto Corsa Competizione dedicated servers. This API enables full control over server instances, configurations, user management, and monitoring through HTTP endpoints.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3000/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All API endpoints (except public ones) require authentication via JWT tokens.
|
||||||
|
|
||||||
|
### Authentication Header
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <your-jwt-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Expiration
|
||||||
|
- Default token lifetime: 24 hours
|
||||||
|
- Tokens should be refreshed before expiration
|
||||||
|
- Failed authentication returns HTTP 401
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
The API implements multiple layers of rate limiting:
|
||||||
|
|
||||||
|
- **Global**: 100 requests per minute per IP
|
||||||
|
- **Authentication**: 5 attempts per 15 minutes per IP
|
||||||
|
- **API Endpoints**: 60 requests per minute per IP
|
||||||
|
|
||||||
|
Rate limit exceeded responses return HTTP 429 with retry information.
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
All API responses follow a consistent JSON format:
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// Response data
|
||||||
|
},
|
||||||
|
"message": "Operation completed successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "ERROR_CODE",
|
||||||
|
"message": "Human readable error message",
|
||||||
|
"details": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Status Codes
|
||||||
|
|
||||||
|
| Status Code | Description |
|
||||||
|
|-------------|-------------|
|
||||||
|
| 200 | OK - Request successful |
|
||||||
|
| 201 | Created - Resource created successfully |
|
||||||
|
| 400 | Bad Request - Invalid request data |
|
||||||
|
| 401 | Unauthorized - Authentication required |
|
||||||
|
| 403 | Forbidden - Insufficient permissions |
|
||||||
|
| 404 | Not Found - Resource not found |
|
||||||
|
| 409 | Conflict - Resource already exists |
|
||||||
|
| 422 | Unprocessable Entity - Validation failed |
|
||||||
|
| 429 | Too Many Requests - Rate limit exceeded |
|
||||||
|
| 500 | Internal Server Error - Server error |
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
```http
|
||||||
|
POST /api/v1/auth/login
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "string",
|
||||||
|
"password": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"token": "jwt-token-string",
|
||||||
|
"user": {
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "string",
|
||||||
|
"role": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "string",
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Register User
|
||||||
|
```http
|
||||||
|
POST /api/v1/auth/register
|
||||||
|
```
|
||||||
|
*Requires: `user.create` permission*
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "string",
|
||||||
|
"password": "string",
|
||||||
|
"roleId": "uuid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Current User
|
||||||
|
```http
|
||||||
|
GET /api/v1/auth/me
|
||||||
|
```
|
||||||
|
*Requires: Authentication*
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "string",
|
||||||
|
"role": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "string",
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Management
|
||||||
|
|
||||||
|
#### List Servers
|
||||||
|
```http
|
||||||
|
GET /api/v1/servers
|
||||||
|
```
|
||||||
|
*Requires: `server.read` permission*
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `page` (integer): Page number (default: 1)
|
||||||
|
- `limit` (integer): Items per page (default: 10)
|
||||||
|
- `search` (string): Search term
|
||||||
|
- `status` (string): Filter by status (running, stopped, error)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "string",
|
||||||
|
"ip": "string",
|
||||||
|
"port": 9600,
|
||||||
|
"path": "string",
|
||||||
|
"serviceName": "string",
|
||||||
|
"status": "string",
|
||||||
|
"dateCreated": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 50,
|
||||||
|
"pages": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create Server
|
||||||
|
```http
|
||||||
|
POST /api/v1/servers
|
||||||
|
```
|
||||||
|
*Requires: `server.create` permission*
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"ip": "string",
|
||||||
|
"port": 9600,
|
||||||
|
"path": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "string",
|
||||||
|
"ip": "string",
|
||||||
|
"port": 9600,
|
||||||
|
"path": "string",
|
||||||
|
"serviceName": "string",
|
||||||
|
"status": "created",
|
||||||
|
"dateCreated": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Server Details
|
||||||
|
```http
|
||||||
|
GET /api/v1/servers/{id}
|
||||||
|
```
|
||||||
|
*Requires: `server.read` permission*
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (integer): Server ID
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "string",
|
||||||
|
"ip": "string",
|
||||||
|
"port": 9600,
|
||||||
|
"path": "string",
|
||||||
|
"serviceName": "string",
|
||||||
|
"status": "string",
|
||||||
|
"dateCreated": "2024-01-01T00:00:00Z",
|
||||||
|
"configs": [],
|
||||||
|
"statistics": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Server
|
||||||
|
```http
|
||||||
|
PUT /api/v1/servers/{id}
|
||||||
|
```
|
||||||
|
*Requires: `server.update` permission*
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (integer): Server ID
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"ip": "string",
|
||||||
|
"port": 9600,
|
||||||
|
"path": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Delete Server
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/servers/{id}
|
||||||
|
```
|
||||||
|
*Requires: `server.delete` permission*
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (integer): Server ID
|
||||||
|
|
||||||
|
#### Start Server
|
||||||
|
```http
|
||||||
|
POST /api/v1/servers/{id}/start
|
||||||
|
```
|
||||||
|
*Requires: `server.control` permission*
|
||||||
|
|
||||||
|
#### Stop Server
|
||||||
|
```http
|
||||||
|
POST /api/v1/servers/{id}/stop
|
||||||
|
```
|
||||||
|
*Requires: `server.control` permission*
|
||||||
|
|
||||||
|
#### Restart Server
|
||||||
|
```http
|
||||||
|
POST /api/v1/servers/{id}/restart
|
||||||
|
```
|
||||||
|
*Requires: `server.control` permission*
|
||||||
|
|
||||||
|
### Configuration Management
|
||||||
|
|
||||||
|
#### Get Configuration File
|
||||||
|
```http
|
||||||
|
GET /api/v1/servers/{id}/config/{file}
|
||||||
|
```
|
||||||
|
*Requires: `config.read` permission*
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (integer): Server ID
|
||||||
|
- `file` (string): Configuration file name (configuration, event, eventRules, settings)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"file": "configuration",
|
||||||
|
"content": {},
|
||||||
|
"lastModified": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Configuration File
|
||||||
|
```http
|
||||||
|
PUT /api/v1/servers/{id}/config/{file}
|
||||||
|
```
|
||||||
|
*Requires: `config.update` permission*
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (integer): Server ID
|
||||||
|
- `file` (string): Configuration file name
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `restart` (boolean): Restart server after update (default: false)
|
||||||
|
- `override` (boolean): Override validation warnings (default: false)
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tcpPort": 9600,
|
||||||
|
"udpPort": 9600,
|
||||||
|
"maxConnections": 30,
|
||||||
|
"registerToLobby": 1,
|
||||||
|
"serverName": "My ACC Server",
|
||||||
|
"password": "",
|
||||||
|
"adminPassword": "admin123",
|
||||||
|
"trackMedalsRequirement": 0,
|
||||||
|
"safetyRatingRequirement": -1,
|
||||||
|
"racecraftRatingRequirement": -1,
|
||||||
|
"configVersion": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Validate Configuration
|
||||||
|
```http
|
||||||
|
POST /api/v1/servers/{id}/config/{file}/validate
|
||||||
|
```
|
||||||
|
*Requires: `config.read` permission*
|
||||||
|
|
||||||
|
**Request Body:** Configuration object to validate
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"valid": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steam Integration
|
||||||
|
|
||||||
|
#### Get Steam Credentials
|
||||||
|
```http
|
||||||
|
GET /api/v1/steam/credentials
|
||||||
|
```
|
||||||
|
*Requires: `steam.read` permission*
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "steam_username",
|
||||||
|
"dateCreated": "2024-01-01T00:00:00Z",
|
||||||
|
"lastUpdated": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Steam Credentials
|
||||||
|
```http
|
||||||
|
PUT /api/v1/steam/credentials
|
||||||
|
```
|
||||||
|
*Requires: `steam.update` permission*
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "steam_username",
|
||||||
|
"password": "steam_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install/Update Server
|
||||||
|
```http
|
||||||
|
POST /api/v1/steam/install
|
||||||
|
```
|
||||||
|
*Requires: `steam.install` permission*
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"serverId": 1,
|
||||||
|
"validate": true,
|
||||||
|
"beta": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
|
||||||
|
#### List Users
|
||||||
|
```http
|
||||||
|
GET /api/v1/users
|
||||||
|
```
|
||||||
|
*Requires: `user.read` permission*
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `page` (integer): Page number
|
||||||
|
- `limit` (integer): Items per page
|
||||||
|
- `search` (string): Search term
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "string",
|
||||||
|
"role": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "string"
|
||||||
|
},
|
||||||
|
"createdAt": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create User
|
||||||
|
```http
|
||||||
|
POST /api/v1/users
|
||||||
|
```
|
||||||
|
*Requires: `user.create` permission*
|
||||||
|
|
||||||
|
#### Update User
|
||||||
|
```http
|
||||||
|
PUT /api/v1/users/{id}
|
||||||
|
```
|
||||||
|
*Requires: `user.update` permission*
|
||||||
|
|
||||||
|
#### Delete User
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/users/{id}
|
||||||
|
```
|
||||||
|
*Requires: `user.delete` permission*
|
||||||
|
|
||||||
|
### Role and Permission Management
|
||||||
|
|
||||||
|
#### List Roles
|
||||||
|
```http
|
||||||
|
GET /api/v1/roles
|
||||||
|
```
|
||||||
|
*Requires: `role.read` permission*
|
||||||
|
|
||||||
|
#### Create Role
|
||||||
|
```http
|
||||||
|
POST /api/v1/roles
|
||||||
|
```
|
||||||
|
*Requires: `role.create` permission*
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"permissions": ["permission1", "permission2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List Permissions
|
||||||
|
```http
|
||||||
|
GET /api/v1/permissions
|
||||||
|
```
|
||||||
|
*Requires: `permission.read` permission*
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "server.create",
|
||||||
|
"description": "Create new servers",
|
||||||
|
"category": "server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring and Analytics
|
||||||
|
|
||||||
|
#### Get Server Statistics
|
||||||
|
```http
|
||||||
|
GET /api/v1/servers/{id}/stats
|
||||||
|
```
|
||||||
|
*Requires: `stats.read` permission*
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `from` (string): Start date (ISO 8601)
|
||||||
|
- `to` (string): End date (ISO 8601)
|
||||||
|
- `granularity` (string): hour, day, week, month
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"totalPlaytime": 3600,
|
||||||
|
"playerCount": [],
|
||||||
|
"sessionTypes": [],
|
||||||
|
"dailyActivity": [],
|
||||||
|
"recentSessions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get System Health
|
||||||
|
```http
|
||||||
|
GET /api/v1/system/health
|
||||||
|
```
|
||||||
|
*Public endpoint*
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"status": "healthy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"uptime": 3600,
|
||||||
|
"database": "connected",
|
||||||
|
"services": {
|
||||||
|
"steam": "available",
|
||||||
|
"nssm": "available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lookup Data
|
||||||
|
|
||||||
|
#### Get Tracks
|
||||||
|
```http
|
||||||
|
GET /api/v1/lookup/tracks
|
||||||
|
```
|
||||||
|
*Public endpoint*
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "monza",
|
||||||
|
"uniquePitBoxes": 29,
|
||||||
|
"privateServerSlots": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Car Models
|
||||||
|
```http
|
||||||
|
GET /api/v1/lookup/cars
|
||||||
|
```
|
||||||
|
*Public endpoint*
|
||||||
|
|
||||||
|
#### Get Driver Categories
|
||||||
|
```http
|
||||||
|
GET /api/v1/lookup/driver-categories
|
||||||
|
```
|
||||||
|
*Public endpoint*
|
||||||
|
|
||||||
|
#### Get Cup Categories
|
||||||
|
```http
|
||||||
|
GET /api/v1/lookup/cup-categories
|
||||||
|
```
|
||||||
|
*Public endpoint*
|
||||||
|
|
||||||
|
#### Get Session Types
|
||||||
|
```http
|
||||||
|
GET /api/v1/lookup/session-types
|
||||||
|
```
|
||||||
|
*Public endpoint*
|
||||||
|
|
||||||
|
## Webhooks
|
||||||
|
|
||||||
|
The API supports webhook notifications for server events:
|
||||||
|
|
||||||
|
### Server Status Changes
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "server.status.changed",
|
||||||
|
"serverId": 1,
|
||||||
|
"serverName": "My Server",
|
||||||
|
"oldStatus": "stopped",
|
||||||
|
"newStatus": "running",
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Updates
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "server.config.updated",
|
||||||
|
"serverId": 1,
|
||||||
|
"serverName": "My Server",
|
||||||
|
"configFile": "configuration",
|
||||||
|
"userId": "uuid",
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `AUTH_REQUIRED` | Authentication required |
|
||||||
|
| `AUTH_INVALID` | Invalid credentials |
|
||||||
|
| `AUTH_EXPIRED` | Token expired |
|
||||||
|
| `PERMISSION_DENIED` | Insufficient permissions |
|
||||||
|
| `VALIDATION_FAILED` | Request validation failed |
|
||||||
|
| `RESOURCE_NOT_FOUND` | Requested resource not found |
|
||||||
|
| `RESOURCE_EXISTS` | Resource already exists |
|
||||||
|
| `RATE_LIMIT_EXCEEDED` | Rate limit exceeded |
|
||||||
|
| `SERVER_ERROR` | Internal server error |
|
||||||
|
| `SERVICE_UNAVAILABLE` | External service unavailable |
|
||||||
|
|
||||||
|
## SDK Examples
|
||||||
|
|
||||||
|
### JavaScript/Node.js
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
class ACCServerManagerAPI {
|
||||||
|
constructor(baseUrl, token) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.token = token;
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServers() {
|
||||||
|
const response = await this.client.get('/servers');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createServer(serverData) {
|
||||||
|
const response = await this.client.post('/servers', serverData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateConfig(serverId, file, config, restart = false) {
|
||||||
|
const response = await this.client.put(
|
||||||
|
`/servers/${serverId}/config/${file}?restart=${restart}`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const api = new ACCServerManagerAPI('http://localhost:3000/api/v1', 'your-jwt-token');
|
||||||
|
const servers = await api.getServers();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class ACCServerManagerAPI:
|
||||||
|
def __init__(self, base_url, token):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.headers = {
|
||||||
|
'Authorization': f'Bearer {token}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_servers(self):
|
||||||
|
response = requests.get(f'{self.base_url}/servers', headers=self.headers)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def create_server(self, server_data):
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/servers',
|
||||||
|
json=server_data,
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
api = ACCServerManagerAPI('http://localhost:3000/api/v1', 'your-jwt-token')
|
||||||
|
servers = api.get_servers()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ACCServerManagerAPI struct {
|
||||||
|
BaseURL string
|
||||||
|
Token string
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewACCServerManagerAPI(baseURL, token string) *ACCServerManagerAPI {
|
||||||
|
return &ACCServerManagerAPI{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
Token: token,
|
||||||
|
Client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *ACCServerManagerAPI) request(method, endpoint string, body interface{}) (*http.Response, error) {
|
||||||
|
var reqBody bytes.Buffer
|
||||||
|
if body != nil {
|
||||||
|
json.NewEncoder(&reqBody).Encode(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, api.BaseURL+endpoint, &reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+api.Token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return api.Client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *ACCServerManagerAPI) GetServers() (interface{}, error) {
|
||||||
|
resp, err := api.request("GET", "/servers", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
1. Store JWT tokens securely (httpOnly cookies for web apps)
|
||||||
|
2. Implement token refresh mechanism
|
||||||
|
3. Handle authentication errors gracefully
|
||||||
|
4. Use HTTPS in production
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
1. Implement exponential backoff for rate-limited requests
|
||||||
|
2. Cache responses when appropriate
|
||||||
|
3. Use batch operations when available
|
||||||
|
4. Monitor rate limit headers
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
1. Always check response status codes
|
||||||
|
2. Handle network errors gracefully
|
||||||
|
3. Implement retry logic for transient errors
|
||||||
|
4. Log errors for debugging
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
1. Use pagination for large datasets
|
||||||
|
2. Implement client-side caching
|
||||||
|
3. Use WebSockets for real-time updates
|
||||||
|
4. Compress request/response bodies
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For API support:
|
||||||
|
- **Documentation**: Check this guide and interactive Swagger UI
|
||||||
|
- **Issues**: Report API bugs via GitHub Issues
|
||||||
|
- **Community**: Join community discussions for help
|
||||||
|
- **Professional Support**: Contact maintainers for enterprise support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This API is versioned. Breaking changes will result in a new API version. Always specify the version in your requests.
|
||||||
395
documentation/CONFIGURATION.md
Normal file
395
documentation/CONFIGURATION.md
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
# Configuration Guide for ACC Server Manager
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide provides comprehensive information about configuring the ACC Server Manager application, including environment variables, server settings, security configurations, and advanced options.
|
||||||
|
|
||||||
|
## 📁 Configuration Files
|
||||||
|
|
||||||
|
### Environment Configuration (.env)
|
||||||
|
|
||||||
|
The primary configuration is handled through environment variables. Create a `.env` file in the root directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy the example file
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration File Hierarchy
|
||||||
|
|
||||||
|
1. **Environment Variables** (highest priority)
|
||||||
|
2. **`.env` file** (medium priority)
|
||||||
|
3. **Default values** (lowest priority)
|
||||||
|
|
||||||
|
## 🔐 Security Configuration
|
||||||
|
|
||||||
|
### Required Security Variables
|
||||||
|
|
||||||
|
These variables are **mandatory** and the application will not start without them:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# JWT Secret - Used for signing authentication tokens
|
||||||
|
# Generate with: openssl rand -base64 64
|
||||||
|
JWT_SECRET=your-super-secure-jwt-secret-minimum-64-characters-long
|
||||||
|
|
||||||
|
# Application Secrets - Used for internal encryption and security
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
APP_SECRET=your-32-character-hex-secret-here
|
||||||
|
APP_SECRET_CODE=your-32-character-hex-secret-code-here
|
||||||
|
|
||||||
|
# Encryption Key - Used for AES-256 encryption (MUST be exactly 32 bytes)
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
ENCRYPTION_KEY=your-exactly-32-byte-encryption-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Server Configuration
|
||||||
|
|
||||||
|
### Basic Server Settings
|
||||||
|
|
||||||
|
```env
|
||||||
|
# HTTP server port
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# CORS allowed origin (comma-separated for multiple origins)
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:5173
|
||||||
|
|
||||||
|
# Database file name (SQLite)
|
||||||
|
DB_NAME=acc.db
|
||||||
|
|
||||||
|
# Default admin password for initial setup (change after first login)
|
||||||
|
PASSWORD=change-this-default-admin-password
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Most other server configuration options (timeouts, request limits, etc.) are handled by application defaults and don't require environment variables.
|
||||||
|
|
||||||
|
## 🗄️ Database Configuration
|
||||||
|
|
||||||
|
The application uses SQLite with minimal configuration required:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database file name (only setting available via environment)
|
||||||
|
DB_NAME=acc.db
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Other database settings like connection timeouts, migration settings, and SQL logging are handled internally by the application and don't require environment variables.
|
||||||
|
|
||||||
|
## 🎮 Steam Integration
|
||||||
|
|
||||||
|
Steam integration settings are managed through the web interface and stored in the database as system configuration. No environment variables are required for Steam integration.
|
||||||
|
|
||||||
|
**Configuration via Web Interface:**
|
||||||
|
- SteamCMD executable path
|
||||||
|
- NSSM executable path
|
||||||
|
- Steam credentials (encrypted in database)
|
||||||
|
- Update schedules and preferences
|
||||||
|
|
||||||
|
**Default Values:**
|
||||||
|
- SteamCMD Path: `c:\steamcmd\steamcmd.exe`
|
||||||
|
- NSSM Path: `.\nssm.exe`
|
||||||
|
|
||||||
|
## 🔥 Windows Service Configuration
|
||||||
|
|
||||||
|
Windows service and firewall configurations are handled internally by the application:
|
||||||
|
|
||||||
|
**Service Management:**
|
||||||
|
- NSSM path configured via web interface
|
||||||
|
- Default service name prefix: `ACC-Server`
|
||||||
|
- Automatic service creation and management
|
||||||
|
|
||||||
|
**Firewall Management:**
|
||||||
|
- Automatic firewall rule creation
|
||||||
|
- Default TCP port range: 9600+
|
||||||
|
- Default UDP port range: 9600+
|
||||||
|
- Rule cleanup on server deletion
|
||||||
|
|
||||||
|
**No environment variables required** - all settings are managed through the system configuration interface.
|
||||||
|
|
||||||
|
## 📊 Logging Configuration
|
||||||
|
|
||||||
|
Logging is handled internally by the application with sensible defaults:
|
||||||
|
|
||||||
|
**Default Logging Behavior:**
|
||||||
|
- Log level: `info` (adjustable via code)
|
||||||
|
- Log format: Structured text format
|
||||||
|
- Log files: Automatic rotation and cleanup
|
||||||
|
- Security events: Automatically logged
|
||||||
|
- Error tracking: Comprehensive error logging
|
||||||
|
|
||||||
|
**No environment variables required** - logging configuration is built into the application.
|
||||||
|
|
||||||
|
## 🚦 Rate Limiting Configuration
|
||||||
|
|
||||||
|
Rate limiting is built into the application with secure defaults:
|
||||||
|
|
||||||
|
**Built-in Rate Limits:**
|
||||||
|
- Global: 100 requests per minute per IP
|
||||||
|
- Authentication: 5 attempts per 15 minutes per IP
|
||||||
|
- API endpoints: 60 requests per minute per IP
|
||||||
|
- Configuration updates: Protected with additional limits
|
||||||
|
|
||||||
|
**No environment variables required** - rate limiting is automatically applied with appropriate limits for security and performance.
|
||||||
|
|
||||||
|
## 📈 Monitoring Configuration
|
||||||
|
|
||||||
|
Monitoring features are built into the application:
|
||||||
|
|
||||||
|
**Available Monitoring:**
|
||||||
|
- Health check endpoint: `/health` (always enabled)
|
||||||
|
- Performance tracking: Built-in performance monitoring
|
||||||
|
- Error tracking: Automatic error logging and tracking
|
||||||
|
- Security monitoring: Authentication and authorization events
|
||||||
|
|
||||||
|
**No environment variables required** - monitoring is automatically enabled with appropriate defaults.
|
||||||
|
|
||||||
|
## 🔄 Backup Configuration
|
||||||
|
|
||||||
|
Backup functionality is handled internally:
|
||||||
|
|
||||||
|
**Automatic Backups:**
|
||||||
|
- Database backup before migrations
|
||||||
|
- Configuration file versioning
|
||||||
|
- Error recovery mechanisms
|
||||||
|
|
||||||
|
**Manual Backups:**
|
||||||
|
- Database files can be copied manually
|
||||||
|
- Configuration export/import via web interface
|
||||||
|
|
||||||
|
**No environment variables required** - backup features are built into the application workflow.
|
||||||
|
|
||||||
|
## 🧪 Development Configuration
|
||||||
|
|
||||||
|
### Development Mode Settings
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Enable development mode (NEVER use in production)
|
||||||
|
DEV_MODE=false
|
||||||
|
|
||||||
|
# Enable debug endpoints
|
||||||
|
DEBUG_ENDPOINTS=false
|
||||||
|
|
||||||
|
# Enable hot reload (requires air)
|
||||||
|
HOT_RELOAD=false
|
||||||
|
|
||||||
|
# Disable security features for testing (DANGEROUS)
|
||||||
|
DISABLE_SECURITY=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Configuration
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Test database name
|
||||||
|
TEST_DB_NAME=acc_test.db
|
||||||
|
|
||||||
|
# Enable test fixtures
|
||||||
|
ENABLE_TEST_FIXTURES=false
|
||||||
|
|
||||||
|
# Test timeout in seconds
|
||||||
|
TEST_TIMEOUT=300
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏭 Production Configuration
|
||||||
|
|
||||||
|
### Production Deployment Settings
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Production mode
|
||||||
|
PRODUCTION=true
|
||||||
|
|
||||||
|
# Enable HTTPS enforcement
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
|
||||||
|
# Security-first configuration
|
||||||
|
SECURITY_STRICT=true
|
||||||
|
|
||||||
|
# Disable debug information
|
||||||
|
DISABLE_DEBUG_INFO=true
|
||||||
|
|
||||||
|
# Enable comprehensive monitoring
|
||||||
|
ENABLE_MONITORING=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Enable response compression
|
||||||
|
ENABLE_COMPRESSION=true
|
||||||
|
|
||||||
|
# Compression level (1-9)
|
||||||
|
COMPRESSION_LEVEL=6
|
||||||
|
|
||||||
|
# Enable response caching
|
||||||
|
ENABLE_CACHING=true
|
||||||
|
|
||||||
|
# Cache TTL in seconds
|
||||||
|
CACHE_TTL=300
|
||||||
|
|
||||||
|
# Maximum cache size in MB
|
||||||
|
CACHE_MAX_SIZE=100
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Advanced Configuration
|
||||||
|
|
||||||
|
### Custom Port Ranges
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Custom TCP port ranges (comma-separated)
|
||||||
|
CUSTOM_TCP_PORTS=9600-9610,9700-9710
|
||||||
|
|
||||||
|
# Custom UDP port ranges (comma-separated)
|
||||||
|
CUSTOM_UDP_PORTS=9600-9610,9700-9710
|
||||||
|
|
||||||
|
# Exclude specific ports (comma-separated)
|
||||||
|
EXCLUDED_PORTS=9605,9705
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Paths
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Custom ACC server installation path
|
||||||
|
ACC_SERVER_PATH=C:\ACC_Server
|
||||||
|
|
||||||
|
# Custom configuration templates path
|
||||||
|
CONFIG_TEMPLATES_PATH=./templates
|
||||||
|
|
||||||
|
# Custom scripts path
|
||||||
|
SCRIPTS_PATH=./scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Settings
|
||||||
|
|
||||||
|
```env
|
||||||
|
# External API endpoints
|
||||||
|
EXTERNAL_API_ENABLED=false
|
||||||
|
EXTERNAL_API_URL=https://api.example.com
|
||||||
|
EXTERNAL_API_KEY=your-api-key-here
|
||||||
|
|
||||||
|
# Webhook notifications
|
||||||
|
WEBHOOK_ENABLED=false
|
||||||
|
WEBHOOK_URL=https://your-webhook-url.com
|
||||||
|
WEBHOOK_SECRET=your-webhook-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Configuration Validation
|
||||||
|
|
||||||
|
### Validation Rules
|
||||||
|
|
||||||
|
The application automatically validates configuration on startup:
|
||||||
|
|
||||||
|
1. **Required Variables**: Must be present and non-empty
|
||||||
|
2. **Numeric Values**: Must be valid numbers within acceptable ranges
|
||||||
|
3. **File Paths**: Must be accessible and have appropriate permissions
|
||||||
|
4. **URLs**: Must be valid URL format
|
||||||
|
5. **Encryption Keys**: Must be exactly 32 bytes for AES-256
|
||||||
|
|
||||||
|
### Configuration Errors
|
||||||
|
|
||||||
|
Common configuration errors and solutions:
|
||||||
|
|
||||||
|
#### "JWT_SECRET must be at least 32 bytes long"
|
||||||
|
- **Solution**: Generate a longer JWT secret using `openssl rand -base64 64`
|
||||||
|
|
||||||
|
#### "ENCRYPTION_KEY must be exactly 32 bytes long"
|
||||||
|
- **Solution**: Generate a 32-byte key using `openssl rand -hex 32`
|
||||||
|
|
||||||
|
#### "Invalid port number"
|
||||||
|
- **Solution**: Ensure port numbers are between 1 and 65535
|
||||||
|
|
||||||
|
#### "SteamCMD not found"
|
||||||
|
- **Solution**: Install SteamCMD and update the `STEAMCMD_PATH` variable
|
||||||
|
|
||||||
|
## 🔧 Configuration Management
|
||||||
|
|
||||||
|
### Environment-Specific Configurations
|
||||||
|
|
||||||
|
#### Development (.env.development)
|
||||||
|
```env
|
||||||
|
DEV_MODE=true
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production (.env.production)
|
||||||
|
```env
|
||||||
|
PRODUCTION=true
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
LOG_LEVEL=warn
|
||||||
|
SECURITY_STRICT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing (.env.test)
|
||||||
|
```env
|
||||||
|
DB_NAME=acc_test.db
|
||||||
|
LOG_LEVEL=error
|
||||||
|
DISABLE_RATE_LIMITING=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Templates
|
||||||
|
|
||||||
|
Create configuration templates for common setups:
|
||||||
|
|
||||||
|
#### Single Server Setup
|
||||||
|
```env
|
||||||
|
# Minimal configuration for single server
|
||||||
|
PORT=3000
|
||||||
|
DB_NAME=acc.db
|
||||||
|
SERVICE_NAME_PREFIX=ACC-Server
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Multi-Server Setup
|
||||||
|
```env
|
||||||
|
# Configuration for multiple servers
|
||||||
|
AUTO_FIREWALL_RULES=true
|
||||||
|
PORT_RANGE_SIZE=20
|
||||||
|
SERVICE_START_TIMEOUT=120
|
||||||
|
```
|
||||||
|
|
||||||
|
#### High-Security Setup
|
||||||
|
```env
|
||||||
|
# Maximum security configuration
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
RATE_LIMIT_AUTH=3
|
||||||
|
SESSION_TIMEOUT=30
|
||||||
|
MAX_LOGIN_ATTEMPTS=3
|
||||||
|
LOCKOUT_DURATION=30
|
||||||
|
SECURITY_STRICT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Security Best Practices
|
||||||
|
|
||||||
|
### Secret Management
|
||||||
|
|
||||||
|
1. **Never commit secrets to version control**
|
||||||
|
2. **Use environment-specific secret files**
|
||||||
|
3. **Rotate secrets regularly**
|
||||||
|
4. **Use secure secret generation methods**
|
||||||
|
5. **Limit access to configuration files**
|
||||||
|
|
||||||
|
### Production Security
|
||||||
|
|
||||||
|
1. **Enable HTTPS enforcement**
|
||||||
|
2. **Configure appropriate CORS origins**
|
||||||
|
3. **Set up proper rate limiting**
|
||||||
|
4. **Enable comprehensive logging**
|
||||||
|
5. **Regular security audits**
|
||||||
|
|
||||||
|
## 📞 Configuration Support
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
For configuration issues:
|
||||||
|
|
||||||
|
1. Check the application logs for specific error messages
|
||||||
|
2. Validate environment variables using the built-in validation
|
||||||
|
3. Refer to the examples in `.env.example`
|
||||||
|
4. Test configuration changes in development first
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
- **Documentation**: Check this guide and other documentation files
|
||||||
|
- **Issues**: Report configuration bugs via GitHub Issues
|
||||||
|
- **Community**: Ask questions in community discussions
|
||||||
|
- **Professional Support**: Contact maintainers for enterprise support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: Always test configuration changes in a development environment before applying them to production.
|
||||||
691
documentation/DEPLOYMENT.md
Normal file
691
documentation/DEPLOYMENT.md
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
# Deployment Guide for ACC Server Manager
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide provides comprehensive instructions for deploying the ACC Server Manager in various environments, from development to production. It covers security considerations, performance optimization, monitoring setup, and maintenance procedures.
|
||||||
|
|
||||||
|
## 🚀 Quick Start Deployment
|
||||||
|
|
||||||
|
### Prerequisites Checklist
|
||||||
|
|
||||||
|
- [ ] Windows 10/11 or Windows Server 2016+
|
||||||
|
- [ ] Go 1.23.0 or later installed
|
||||||
|
- [ ] Administrative privileges
|
||||||
|
- [ ] Valid Steam account
|
||||||
|
- [ ] Internet connection for Steam downloads
|
||||||
|
|
||||||
|
### Minimum System Requirements
|
||||||
|
|
||||||
|
| Component | Minimum | Recommended |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| **CPU** | 2 cores | 4+ cores |
|
||||||
|
| **RAM** | 4 GB | 8+ GB |
|
||||||
|
| **Storage** | 10 GB free | 50+ GB SSD |
|
||||||
|
| **Network** | 10 Mbps | 100+ Mbps |
|
||||||
|
|
||||||
|
## 📦 Installation Methods
|
||||||
|
|
||||||
|
### Method 1: Binary Deployment (Recommended)
|
||||||
|
|
||||||
|
1. **Download Release Binary**
|
||||||
|
```bash
|
||||||
|
# Download the latest release from GitHub
|
||||||
|
# Extract to your installation directory
|
||||||
|
cd C:\ACC-Server-Manager
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Environment**
|
||||||
|
```bash
|
||||||
|
copy .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Generate Secrets**
|
||||||
|
```bash
|
||||||
|
# Generate JWT secret
|
||||||
|
openssl rand -base64 64
|
||||||
|
|
||||||
|
# Generate app secrets
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Generate encryption key
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run Application**
|
||||||
|
```bash
|
||||||
|
.\acc-server-manager.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Source Code Deployment
|
||||||
|
|
||||||
|
1. **Clone Repository**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/FJurmanovic/acc-server-manager.git
|
||||||
|
cd acc-server-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Dependencies**
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
go mod verify
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build Application**
|
||||||
|
```bash
|
||||||
|
# Development build
|
||||||
|
go build -o acc-server-manager.exe cmd/api/main.go
|
||||||
|
|
||||||
|
# Production build (optimized)
|
||||||
|
go build -ldflags="-w -s" -o acc-server-manager.exe cmd/api/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Configure and Run**
|
||||||
|
```bash
|
||||||
|
copy .env.example .env
|
||||||
|
# Configure your .env file
|
||||||
|
.\acc-server-manager.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Environment Configuration
|
||||||
|
|
||||||
|
### Production Environment Variables
|
||||||
|
|
||||||
|
Create a production `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# ========================================
|
||||||
|
# PRODUCTION CONFIGURATION
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Security (REQUIRED - Generate unique values)
|
||||||
|
JWT_SECRET=your-production-jwt-secret-64-chars-minimum
|
||||||
|
APP_SECRET=your-production-app-secret-32-chars
|
||||||
|
APP_SECRET_CODE=your-production-secret-code-32-chars
|
||||||
|
ENCRYPTION_KEY=your-production-encryption-key-32-bytes
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=8080
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PRODUCTION=true
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_NAME=acc_production.db
|
||||||
|
DB_PATH=./data
|
||||||
|
|
||||||
|
# CORS (Set to your actual domain)
|
||||||
|
CORS_ALLOWED_ORIGIN=https://yourdomain.com
|
||||||
|
|
||||||
|
# Security Settings
|
||||||
|
RATE_LIMIT_GLOBAL=1000
|
||||||
|
RATE_LIMIT_AUTH=10
|
||||||
|
SESSION_TIMEOUT=120
|
||||||
|
MAX_LOGIN_ATTEMPTS=5
|
||||||
|
LOCKOUT_DURATION=30
|
||||||
|
|
||||||
|
# Steam Configuration
|
||||||
|
STEAMCMD_PATH=C:\steamcmd\steamcmd.exe
|
||||||
|
NSSM_PATH=C:\nssm\nssm.exe
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=warn
|
||||||
|
LOG_FILE=./logs/production.log
|
||||||
|
LOG_MAX_SIZE=100
|
||||||
|
LOG_MAX_FILES=10
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
HEALTH_CHECK_ENABLED=true
|
||||||
|
METRICS_ENABLED=true
|
||||||
|
PERFORMANCE_MONITORING=true
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
AUTO_BACKUP=true
|
||||||
|
BACKUP_INTERVAL=12
|
||||||
|
BACKUP_RETENTION=30
|
||||||
|
BACKUP_DIR=./backups
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Environment Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# ========================================
|
||||||
|
# DEVELOPMENT CONFIGURATION
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Security (Use secure values even in dev)
|
||||||
|
JWT_SECRET=dev-jwt-secret-but-still-secure-64-chars-minimum
|
||||||
|
APP_SECRET=dev-app-secret-32-chars-here
|
||||||
|
APP_SECRET_CODE=dev-secret-code-32-chars-here
|
||||||
|
ENCRYPTION_KEY=dev-encryption-key-32-bytes-here
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=3000
|
||||||
|
HOST=localhost
|
||||||
|
DEV_MODE=true
|
||||||
|
DEBUG_ENDPOINTS=true
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_NAME=acc_dev.db
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:3000,http://localhost:5173
|
||||||
|
|
||||||
|
# Relaxed Security (Development Only)
|
||||||
|
RATE_LIMIT_GLOBAL=1000
|
||||||
|
DISABLE_SECURITY=false
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
LOG_COLORS=true
|
||||||
|
ENABLE_SQL_LOGGING=true
|
||||||
|
|
||||||
|
# Development Tools
|
||||||
|
HOT_RELOAD=true
|
||||||
|
ENABLE_TEST_FIXTURES=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Security Hardening
|
||||||
|
|
||||||
|
### SSL/TLS Configuration
|
||||||
|
|
||||||
|
1. **Obtain SSL Certificate**
|
||||||
|
```bash
|
||||||
|
# Option 1: Let's Encrypt (Free)
|
||||||
|
certbot certonly --webroot -w /var/www/html -d yourdomain.com
|
||||||
|
|
||||||
|
# Option 2: Commercial Certificate
|
||||||
|
# Purchase and install certificate from CA
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Reverse Proxy (Nginx)**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/certificate.crt;
|
||||||
|
ssl_certificate_key /path/to/private.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE+AESGCM:ECDHE+AES256:ECDHE+AES128:!aNULL:!MD5:!DSS;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure Application for SSL**
|
||||||
|
```env
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
CORS_ALLOWED_ORIGIN=https://yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Firewall Configuration
|
||||||
|
|
||||||
|
1. **Windows Firewall Rules**
|
||||||
|
```powershell
|
||||||
|
# Allow application through Windows Firewall
|
||||||
|
New-NetFirewallRule -DisplayName "ACC Server Manager" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow
|
||||||
|
|
||||||
|
# Allow ACC server ports (adjust range as needed)
|
||||||
|
New-NetFirewallRule -DisplayName "ACC Servers TCP" -Direction Inbound -Protocol TCP -LocalPort 9600-9700 -Action Allow
|
||||||
|
New-NetFirewallRule -DisplayName "ACC Servers UDP" -Direction Inbound -Protocol UDP -LocalPort 9600-9700 -Action Allow
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Network Security Groups (Azure)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"securityRules": [
|
||||||
|
{
|
||||||
|
"name": "AllowHTTPS",
|
||||||
|
"properties": {
|
||||||
|
"protocol": "TCP",
|
||||||
|
"sourcePortRange": "*",
|
||||||
|
"destinationPortRange": "443",
|
||||||
|
"sourceAddressPrefix": "*",
|
||||||
|
"destinationAddressPrefix": "*",
|
||||||
|
"access": "Allow",
|
||||||
|
"priority": 1000,
|
||||||
|
"direction": "Inbound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Access Control
|
||||||
|
|
||||||
|
1. **Create Dedicated Service Account**
|
||||||
|
```powershell
|
||||||
|
# Create service account
|
||||||
|
New-LocalUser -Name "ACCServiceUser" -Description "ACC Server Manager Service Account" -NoPassword
|
||||||
|
Add-LocalGroupMember -Group "Users" -Member "ACCServiceUser"
|
||||||
|
|
||||||
|
# Set permissions on application directory
|
||||||
|
icacls "C:\ACC-Server-Manager" /grant "ACCServiceUser:(OI)(CI)F"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Service Permissions**
|
||||||
|
```powershell
|
||||||
|
# Grant service logon rights
|
||||||
|
secedit /export /cfg security.inf
|
||||||
|
# Edit security.inf to add ACCServiceUser to SeServiceLogonRight
|
||||||
|
secedit /configure /db security.sdb /cfg security.inf
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Service Installation
|
||||||
|
|
||||||
|
### Windows Service with NSSM
|
||||||
|
|
||||||
|
1. **Install NSSM**
|
||||||
|
```bash
|
||||||
|
# Download NSSM from https://nssm.cc/
|
||||||
|
# Extract nssm.exe to C:\nssm\
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Service**
|
||||||
|
```powershell
|
||||||
|
# Install service
|
||||||
|
C:\nssm\nssm.exe install "ACCServerManager" "C:\ACC-Server-Manager\acc-server-manager.exe"
|
||||||
|
|
||||||
|
# Configure service
|
||||||
|
C:\nssm\nssm.exe set "ACCServerManager" DisplayName "ACC Server Manager"
|
||||||
|
C:\nssm\nssm.exe set "ACCServerManager" Description "Assetto Corsa Competizione Server Manager"
|
||||||
|
C:\nssm\nssm.exe set "ACCServerManager" Start SERVICE_AUTO_START
|
||||||
|
C:\nssm\nssm.exe set "ACCServerManager" AppDirectory "C:\ACC-Server-Manager"
|
||||||
|
C:\nssm\nssm.exe set "ACCServerManager" ObjectName ".\ACCServiceUser" "password"
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
C:\nssm\nssm.exe set "ACCServerManager" AppStdout "C:\ACC-Server-Manager\logs\service.log"
|
||||||
|
C:\nssm\nssm.exe set "ACCServerManager" AppStderr "C:\ACC-Server-Manager\logs\service-error.log"
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
C:\nssm\nssm.exe start "ACCServerManager"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Service Management**
|
||||||
|
```powershell
|
||||||
|
# Check service status
|
||||||
|
Get-Service -Name "ACCServerManager"
|
||||||
|
|
||||||
|
# Start/Stop service
|
||||||
|
Start-Service -Name "ACCServerManager"
|
||||||
|
Stop-Service -Name "ACCServerManager"
|
||||||
|
|
||||||
|
# Remove service (if needed)
|
||||||
|
C:\nssm\nssm.exe remove "ACCServerManager" confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Systemd Service (Linux/WSL)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=ACC Server Manager
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=accmanager
|
||||||
|
WorkingDirectory=/opt/acc-server-manager
|
||||||
|
ExecStart=/opt/acc-server-manager/acc-server-manager
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
EnvironmentFile=/opt/acc-server-manager/.env
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Monitoring Setup
|
||||||
|
|
||||||
|
### Health Check Monitoring
|
||||||
|
|
||||||
|
1. **Configure Health Checks**
|
||||||
|
```env
|
||||||
|
HEALTH_CHECK_ENABLED=true
|
||||||
|
HEALTH_CHECK_PATH=/health
|
||||||
|
HEALTH_CHECK_TIMEOUT=10
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **External Monitoring (UptimeRobot)**
|
||||||
|
```bash
|
||||||
|
# Monitor endpoint: https://yourdomain.com/health
|
||||||
|
# Expected response: 200 OK with JSON health status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Management
|
||||||
|
|
||||||
|
1. **Log Rotation Configuration**
|
||||||
|
```env
|
||||||
|
LOG_MAX_SIZE=100
|
||||||
|
LOG_MAX_FILES=10
|
||||||
|
LOG_MAX_AGE=30
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Centralized Logging (Optional)**
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml for ELK Stack
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
elasticsearch:
|
||||||
|
image: elasticsearch:7.14.0
|
||||||
|
logstash:
|
||||||
|
image: logstash:7.14.0
|
||||||
|
kibana:
|
||||||
|
image: kibana:7.14.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
|
||||||
|
1. **Enable Metrics**
|
||||||
|
```env
|
||||||
|
METRICS_ENABLED=true
|
||||||
|
METRICS_PORT=9090
|
||||||
|
PERFORMANCE_MONITORING=true
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Prometheus Configuration**
|
||||||
|
```yaml
|
||||||
|
# prometheus.yml
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'acc-server-manager'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Database Management
|
||||||
|
|
||||||
|
### Database Backup Strategy
|
||||||
|
|
||||||
|
1. **Automated Backups**
|
||||||
|
```env
|
||||||
|
AUTO_BACKUP=true
|
||||||
|
BACKUP_INTERVAL=12
|
||||||
|
BACKUP_RETENTION=30
|
||||||
|
BACKUP_DIR=./backups
|
||||||
|
BACKUP_COMPRESS=true
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Manual Backup**
|
||||||
|
```powershell
|
||||||
|
# Create manual backup
|
||||||
|
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||||
|
Copy-Item "acc.db" "backups/acc-backup-$timestamp.db"
|
||||||
|
|
||||||
|
# Compress backup
|
||||||
|
Compress-Archive "backups/acc-backup-$timestamp.db" "backups/acc-backup-$timestamp.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Backup Verification**
|
||||||
|
```bash
|
||||||
|
# Test backup integrity
|
||||||
|
sqlite3 backup.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Migration
|
||||||
|
|
||||||
|
1. **Pre-Migration Backup**
|
||||||
|
```bash
|
||||||
|
# Always backup before migration
|
||||||
|
copy acc.db acc-pre-migration-backup.db
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Migration Process**
|
||||||
|
```bash
|
||||||
|
# Migration runs automatically on startup
|
||||||
|
# Check logs for migration status
|
||||||
|
tail -f logs/app.log | grep -i migration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Load Balancing (High Availability)
|
||||||
|
|
||||||
|
### Multiple Instance Setup
|
||||||
|
|
||||||
|
1. **Load Balancer Configuration (HAProxy)**
|
||||||
|
```haproxy
|
||||||
|
global
|
||||||
|
daemon
|
||||||
|
|
||||||
|
defaults
|
||||||
|
mode http
|
||||||
|
timeout connect 5000ms
|
||||||
|
timeout client 50000ms
|
||||||
|
timeout server 50000ms
|
||||||
|
|
||||||
|
frontend acc_frontend
|
||||||
|
bind *:80
|
||||||
|
default_backend acc_servers
|
||||||
|
|
||||||
|
backend acc_servers
|
||||||
|
balance roundrobin
|
||||||
|
server acc1 10.0.0.10:8080 check
|
||||||
|
server acc2 10.0.0.11:8080 check
|
||||||
|
server acc3 10.0.0.12:8080 check
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Shared Database Setup**
|
||||||
|
```bash
|
||||||
|
# Use network-attached storage for database
|
||||||
|
# Mount shared volume on all instances
|
||||||
|
net use Z: \\fileserver\acc-shared
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Clustering
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Redis for session storage
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
SESSION_STORE=redis
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Maintenance Procedures
|
||||||
|
|
||||||
|
### Regular Maintenance Tasks
|
||||||
|
|
||||||
|
1. **Daily Tasks**
|
||||||
|
```powershell
|
||||||
|
# Check service status
|
||||||
|
Get-Service -Name "ACCServerManager"
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
Get-WmiObject -Class Win32_LogicalDisk | Select-Object DeviceID, Size, FreeSpace
|
||||||
|
|
||||||
|
# Review error logs
|
||||||
|
Get-Content "logs/error.log" -Tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Weekly Tasks**
|
||||||
|
```powershell
|
||||||
|
# Update system patches
|
||||||
|
Install-Module PSWindowsUpdate
|
||||||
|
Get-WUInstall -AcceptAll -AutoReboot
|
||||||
|
|
||||||
|
# Clean old log files
|
||||||
|
Get-ChildItem "logs\" -Name "*.log.*" | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-30)} | Remove-Item
|
||||||
|
|
||||||
|
# Verify backup integrity
|
||||||
|
sqlite3 backups/latest.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Monthly Tasks**
|
||||||
|
```powershell
|
||||||
|
# Update dependencies
|
||||||
|
go get -u ./...
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Security scan
|
||||||
|
go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
|
||||||
|
gosec ./...
|
||||||
|
|
||||||
|
# Performance review
|
||||||
|
# Review metrics and optimize based on usage patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Procedures
|
||||||
|
|
||||||
|
1. **Backup Current Installation**
|
||||||
|
```bash
|
||||||
|
# Stop service
|
||||||
|
Stop-Service -Name "ACCServerManager"
|
||||||
|
|
||||||
|
# Backup application
|
||||||
|
Copy-Item -Recurse "C:\ACC-Server-Manager" "C:\ACC-Server-Manager-Backup-$(Get-Date -Format 'yyyyMMdd')"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy New Version**
|
||||||
|
```bash
|
||||||
|
# Download new version
|
||||||
|
# Replace executable
|
||||||
|
# Update configuration if needed
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
Start-Service -Name "ACCServerManager"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rollback Procedure**
|
||||||
|
```bash
|
||||||
|
# Stop service
|
||||||
|
Stop-Service -Name "ACCServerManager"
|
||||||
|
|
||||||
|
# Restore backup
|
||||||
|
Remove-Item -Recurse "C:\ACC-Server-Manager"
|
||||||
|
Copy-Item -Recurse "C:\ACC-Server-Manager-Backup-$(Get-Date -Format 'yyyyMMdd')" "C:\ACC-Server-Manager"
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
Start-Service -Name "ACCServerManager"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Service Won't Start**
|
||||||
|
```powershell
|
||||||
|
# Check service status
|
||||||
|
Get-Service -Name "ACCServerManager"
|
||||||
|
|
||||||
|
# Check service logs
|
||||||
|
Get-Content "logs/service-error.log" -Tail 50
|
||||||
|
|
||||||
|
# Check Windows Event Log
|
||||||
|
Get-EventLog -LogName System -Source "ACCServerManager" -Newest 10
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Database Connection Issues**
|
||||||
|
```bash
|
||||||
|
# Check database file permissions
|
||||||
|
icacls acc.db
|
||||||
|
|
||||||
|
# Test database connection
|
||||||
|
sqlite3 acc.db ".tables"
|
||||||
|
|
||||||
|
# Check for database locks
|
||||||
|
lsof acc.db # Linux
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Steam Integration Issues**
|
||||||
|
```bash
|
||||||
|
# Verify SteamCMD installation
|
||||||
|
C:\steamcmd\steamcmd.exe +quit
|
||||||
|
|
||||||
|
# Check Steam credentials
|
||||||
|
# Review Steam-related logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
1. **High CPU Usage**
|
||||||
|
```bash
|
||||||
|
# Check for resource-intensive operations
|
||||||
|
# Monitor process performance
|
||||||
|
Get-Process -Name "acc-server-manager" | Select-Object CPU, WorkingSet
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Memory Leaks**
|
||||||
|
```bash
|
||||||
|
# Monitor memory usage over time
|
||||||
|
# Enable detailed memory profiling
|
||||||
|
go tool pprof http://localhost:8080/debug/pprof/heap
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Database Performance**
|
||||||
|
```sql
|
||||||
|
-- Analyze database performance
|
||||||
|
PRAGMA table_info(servers);
|
||||||
|
EXPLAIN QUERY PLAN SELECT * FROM servers WHERE status = 'running';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Support and Resources
|
||||||
|
|
||||||
|
### Documentation Resources
|
||||||
|
- [README.md](../README.md) - Getting started guide
|
||||||
|
- [SECURITY.md](SECURITY.md) - Security guidelines
|
||||||
|
- [API.md](API.md) - API documentation
|
||||||
|
- [CONFIGURATION.md](CONFIGURATION.md) - Configuration reference
|
||||||
|
|
||||||
|
### Community Support
|
||||||
|
- **GitHub Issues** - Bug reports and feature requests
|
||||||
|
- **Discord Community** - Real-time community support
|
||||||
|
- **Wiki** - Community-maintained documentation
|
||||||
|
|
||||||
|
### Professional Support
|
||||||
|
- **Enterprise Support** - Professional deployment assistance
|
||||||
|
- **Consulting Services** - Custom implementation and optimization
|
||||||
|
- **Training** - Team training and best practices
|
||||||
|
|
||||||
|
### Emergency Contacts
|
||||||
|
```
|
||||||
|
Production Issues: support@yourdomain.com
|
||||||
|
Security Issues: security@yourdomain.com
|
||||||
|
Emergency Hotline: +1-XXX-XXX-XXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Deployment Checklist
|
||||||
|
|
||||||
|
### Pre-Deployment
|
||||||
|
- [ ] System requirements verified
|
||||||
|
- [ ] Dependencies installed
|
||||||
|
- [ ] Secrets generated and secured
|
||||||
|
- [ ] Configuration reviewed
|
||||||
|
- [ ] Security hardening applied
|
||||||
|
- [ ] Backup strategy implemented
|
||||||
|
- [ ] Monitoring configured
|
||||||
|
|
||||||
|
### Post-Deployment
|
||||||
|
- [ ] Service running successfully
|
||||||
|
- [ ] Health checks passing
|
||||||
|
- [ ] Logs being written correctly
|
||||||
|
- [ ] Database accessible
|
||||||
|
- [ ] API endpoints responding
|
||||||
|
- [ ] Frontend integration working
|
||||||
|
- [ ] Monitoring alerts configured
|
||||||
|
- [ ] Documentation updated
|
||||||
|
|
||||||
|
### Production Readiness
|
||||||
|
- [ ] SSL/TLS configured
|
||||||
|
- [ ] Firewall rules applied
|
||||||
|
- [ ] Performance monitoring active
|
||||||
|
- [ ] Backup procedures tested
|
||||||
|
- [ ] Update procedures documented
|
||||||
|
- [ ] Disaster recovery plan created
|
||||||
|
- [ ] Team training completed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: Always test deployments in a staging environment before applying to production!
|
||||||
264
documentation/SECURITY.md
Normal file
264
documentation/SECURITY.md
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
# Security Guide for ACC Server Manager
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the security features, best practices, and requirements for the ACC Server Manager application. Following these guidelines is essential for maintaining a secure deployment.
|
||||||
|
|
||||||
|
## 🔐 Authentication & Authorization
|
||||||
|
|
||||||
|
### JWT Token Security
|
||||||
|
|
||||||
|
- **Secret Key**: Must be at least 32 bytes long and cryptographically secure
|
||||||
|
- **Token Expiration**: Default 24 hours, configurable via environment
|
||||||
|
- **Refresh Strategy**: Implement token refresh before expiration
|
||||||
|
- **Storage**: Store tokens securely (httpOnly cookies recommended for web)
|
||||||
|
|
||||||
|
### Password Security
|
||||||
|
|
||||||
|
- **Hashing**: Uses bcrypt with cost factor 12
|
||||||
|
- **Requirements**: Minimum 8 characters, must include uppercase, lowercase, digit, and special character
|
||||||
|
- **Validation**: Real-time strength validation during registration/update
|
||||||
|
- **Storage**: Never store plain text passwords
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
- **Global**: 100 requests per minute per IP
|
||||||
|
- **Authentication**: 5 attempts per 15 minutes per IP+User-Agent
|
||||||
|
- **API Endpoints**: 60 requests per minute per IP
|
||||||
|
- **Customizable**: Configurable via environment variables
|
||||||
|
|
||||||
|
## 🛡️ Security Headers
|
||||||
|
|
||||||
|
The application automatically sets the following security headers:
|
||||||
|
|
||||||
|
- `X-Content-Type-Options: nosniff`
|
||||||
|
- `X-Frame-Options: DENY`
|
||||||
|
- `X-XSS-Protection: 1; mode=block`
|
||||||
|
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||||
|
- `Content-Security-Policy: [configured policy]`
|
||||||
|
- `Permissions-Policy: [restricted permissions]`
|
||||||
|
|
||||||
|
## 🔒 Data Protection
|
||||||
|
|
||||||
|
### Encryption
|
||||||
|
|
||||||
|
- **Algorithm**: AES-256-GCM for sensitive data
|
||||||
|
- **Key Management**: 32-byte keys from environment variables
|
||||||
|
- **Usage**: Steam credentials and other sensitive configuration data
|
||||||
|
|
||||||
|
### Database Security
|
||||||
|
|
||||||
|
- **SQLite**: Default database with file-level security
|
||||||
|
- **Migrations**: Automatic password security upgrades
|
||||||
|
- **Backup**: Encrypted backups with retention policies
|
||||||
|
|
||||||
|
## 🌐 Network Security
|
||||||
|
|
||||||
|
### HTTPS
|
||||||
|
|
||||||
|
- **Production**: HTTPS enforced in production environments
|
||||||
|
- **Certificates**: Use valid SSL/TLS certificates
|
||||||
|
- **Redirection**: Automatic HTTP to HTTPS redirect
|
||||||
|
|
||||||
|
### CORS Configuration
|
||||||
|
|
||||||
|
- **Origins**: Configured per environment
|
||||||
|
- **Headers**: Properly configured for API access
|
||||||
|
- **Credentials**: Enabled for authenticated requests
|
||||||
|
|
||||||
|
### Firewall Rules
|
||||||
|
|
||||||
|
- **Automatic**: Creates Windows Firewall rules for server ports
|
||||||
|
- **Management**: Centralized firewall rule management
|
||||||
|
- **Cleanup**: Automatic rule removal when servers are deleted
|
||||||
|
|
||||||
|
## 🚨 Input Validation & Sanitization
|
||||||
|
|
||||||
|
### Request Validation
|
||||||
|
|
||||||
|
- **Content-Type**: Validates expected content types
|
||||||
|
- **Size Limits**: 10MB request body limit
|
||||||
|
- **User-Agent**: Blocks suspicious user agents
|
||||||
|
- **Timeout**: 30-second request timeout
|
||||||
|
|
||||||
|
### Input Sanitization
|
||||||
|
|
||||||
|
- **XSS Prevention**: Removes dangerous HTML/JavaScript patterns
|
||||||
|
- **SQL Injection**: Uses parameterized queries
|
||||||
|
- **Path Traversal**: Validates file paths and names
|
||||||
|
|
||||||
|
## 📊 Monitoring & Logging
|
||||||
|
|
||||||
|
### Security Events
|
||||||
|
|
||||||
|
- **Authentication**: All login attempts (success/failure)
|
||||||
|
- **Authorization**: Permission checks and violations
|
||||||
|
- **Rate Limiting**: Blocked requests and patterns
|
||||||
|
- **Suspicious Activity**: Automated threat detection
|
||||||
|
|
||||||
|
### Log Security
|
||||||
|
|
||||||
|
- **Sensitive Data**: Never logs passwords or tokens
|
||||||
|
- **Format**: Structured logging with security context
|
||||||
|
- **Retention**: Configurable log retention policies
|
||||||
|
- **Access**: Restricted access to log files
|
||||||
|
|
||||||
|
## ⚙️ Environment Configuration
|
||||||
|
|
||||||
|
### Required Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Critical Security Settings
|
||||||
|
JWT_SECRET=<64-character-base64-string>
|
||||||
|
APP_SECRET=<32-character-hex-string>
|
||||||
|
APP_SECRET_CODE=<32-character-hex-string>
|
||||||
|
ENCRYPTION_KEY=<32-character-hex-string>
|
||||||
|
|
||||||
|
# Security Features
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
RATE_LIMIT_GLOBAL=100
|
||||||
|
RATE_LIMIT_AUTH=5
|
||||||
|
SESSION_TIMEOUT=60
|
||||||
|
MAX_LOGIN_ATTEMPTS=5
|
||||||
|
LOCKOUT_DURATION=15
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secret Generation
|
||||||
|
|
||||||
|
Generate secure secrets using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JWT Secret (Base64, 64 bytes)
|
||||||
|
openssl rand -base64 64
|
||||||
|
|
||||||
|
# Application Secrets (Hex, 32 bytes)
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Encryption Key (Hex, 32 bytes)
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Security Migrations
|
||||||
|
|
||||||
|
### Password Security Upgrade
|
||||||
|
|
||||||
|
The application includes an automatic migration that:
|
||||||
|
|
||||||
|
1. Upgrades old encrypted passwords to bcrypt hashes
|
||||||
|
2. Maintains data integrity during the process
|
||||||
|
3. Provides rollback protection
|
||||||
|
4. Logs migration status and errors
|
||||||
|
|
||||||
|
### Migration Safety
|
||||||
|
|
||||||
|
- **Backup**: Automatically creates password backups
|
||||||
|
- **Validation**: Verifies password strength requirements
|
||||||
|
- **Recovery**: Handles corrupted or invalid passwords
|
||||||
|
- **Logging**: Detailed migration logs for auditing
|
||||||
|
|
||||||
|
## 🚀 Deployment Security
|
||||||
|
|
||||||
|
### Production Checklist
|
||||||
|
|
||||||
|
- [ ] Generate unique secrets for production
|
||||||
|
- [ ] Enable HTTPS with valid certificates
|
||||||
|
- [ ] Configure appropriate CORS origins
|
||||||
|
- [ ] Set up proper firewall rules
|
||||||
|
- [ ] Enable security monitoring and alerting
|
||||||
|
- [ ] Configure secure backup strategies
|
||||||
|
- [ ] Review and adjust rate limits
|
||||||
|
- [ ] Set up log monitoring and analysis
|
||||||
|
- [ ] Test security configurations
|
||||||
|
- [ ] Document security procedures
|
||||||
|
|
||||||
|
### Container Security (if applicable)
|
||||||
|
|
||||||
|
- **Base Images**: Use official, minimal base images
|
||||||
|
- **User Privileges**: Run as non-root user
|
||||||
|
- **Secrets**: Use container secret management
|
||||||
|
- **Network**: Isolate containers appropriately
|
||||||
|
|
||||||
|
## 🔍 Security Testing
|
||||||
|
|
||||||
|
### Automated Testing
|
||||||
|
|
||||||
|
- **Dependencies**: Regular security scanning of dependencies
|
||||||
|
- **SAST**: Static application security testing
|
||||||
|
- **DAST**: Dynamic application security testing
|
||||||
|
- **Penetration Testing**: Regular security assessments
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
- **Authentication Bypass**: Test authentication mechanisms
|
||||||
|
- **Authorization**: Verify permission controls
|
||||||
|
- **Input Validation**: Test input sanitization
|
||||||
|
- **Rate Limiting**: Verify rate limiting effectiveness
|
||||||
|
|
||||||
|
## 🚨 Incident Response
|
||||||
|
|
||||||
|
### Security Incident Procedures
|
||||||
|
|
||||||
|
1. **Detection**: Monitor logs and alerts
|
||||||
|
2. **Assessment**: Evaluate impact and scope
|
||||||
|
3. **Containment**: Isolate affected systems
|
||||||
|
4. **Eradication**: Remove threats and vulnerabilities
|
||||||
|
5. **Recovery**: Restore normal operations
|
||||||
|
6. **Lessons Learned**: Document and improve
|
||||||
|
|
||||||
|
### Emergency Contacts
|
||||||
|
|
||||||
|
- **Security Team**: [Configure your security team contacts]
|
||||||
|
- **System Administrators**: [Configure admin contacts]
|
||||||
|
- **Management**: [Configure management contacts]
|
||||||
|
|
||||||
|
## 📋 Security Maintenance
|
||||||
|
|
||||||
|
### Regular Tasks
|
||||||
|
|
||||||
|
- **Weekly**: Review security logs and alerts
|
||||||
|
- **Monthly**: Update dependencies and security patches
|
||||||
|
- **Quarterly**: Security configuration review
|
||||||
|
- **Annually**: Comprehensive security assessment
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
- **Failed Logins**: Monitor authentication failures
|
||||||
|
- **Rate Limit Hits**: Track rate limiting events
|
||||||
|
- **Error Patterns**: Identify suspicious error patterns
|
||||||
|
- **Performance**: Monitor for DoS attacks
|
||||||
|
|
||||||
|
## 🔗 Additional Resources
|
||||||
|
|
||||||
|
### Security Standards
|
||||||
|
|
||||||
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||||
|
- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework)
|
||||||
|
- [CIS Controls](https://www.cisecurity.org/controls/)
|
||||||
|
|
||||||
|
### Go Security
|
||||||
|
|
||||||
|
- [Go Security Policy](https://golang.org/security)
|
||||||
|
- [Secure Coding Practices](https://github.com/OWASP/Go-SCP)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- [Fiber Security](https://docs.gofiber.io/api/middleware/helmet)
|
||||||
|
- [GORM Security](https://gorm.io/docs/security.html)
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
For security questions or concerns:
|
||||||
|
|
||||||
|
- **Security Issues**: Report via private channels
|
||||||
|
- **Documentation**: Refer to this guide and code comments
|
||||||
|
- **Updates**: Monitor security advisories for dependencies
|
||||||
|
|
||||||
|
## 🔄 Version History
|
||||||
|
|
||||||
|
- **v1.0.0**: Initial security implementation
|
||||||
|
- **v1.1.0**: Added password security migration
|
||||||
|
- **v1.2.0**: Enhanced rate limiting and monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Important**: This security guide should be reviewed and updated regularly as the application evolves and new security threats emerge.
|
||||||
4
go.mod
4
go.mod
@@ -5,11 +5,13 @@ go 1.23.0
|
|||||||
require (
|
require (
|
||||||
github.com/gofiber/fiber/v2 v2.52.8
|
github.com/gofiber/fiber/v2 v2.52.8
|
||||||
github.com/gofiber/swagger v1.1.0
|
github.com/gofiber/swagger v1.1.0
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c
|
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c
|
||||||
github.com/swaggo/swag v1.16.3
|
github.com/swaggo/swag v1.16.3
|
||||||
go.uber.org/dig v1.17.1
|
go.uber.org/dig v1.17.1
|
||||||
|
golang.org/x/crypto v0.39.0
|
||||||
golang.org/x/sync v0.15.0
|
golang.org/x/sync v0.15.0
|
||||||
golang.org/x/text v0.26.0
|
golang.org/x/text v0.26.0
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/sqlite v1.5.6
|
||||||
@@ -23,7 +25,6 @@ require (
|
|||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
@@ -38,7 +39,6 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.33.0 // indirect
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
9
go.sum
9
go.sum
@@ -69,23 +69,16 @@ go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
|||||||
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
|
||||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
|
||||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ func Init(di *dig.Container, app *fiber.App) {
|
|||||||
// Protected routes
|
// Protected routes
|
||||||
groups := app.Group(configs.Prefix)
|
groups := app.Group(configs.Prefix)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
serverIdGroup := groups.Group("/server/:id")
|
serverIdGroup := groups.Group("/server/:id")
|
||||||
routeGroups := &common.RouteGroups{
|
routeGroups := &common.RouteGroups{
|
||||||
Api: groups.Group("/api"),
|
Api: groups.Group("/api"),
|
||||||
@@ -32,20 +30,12 @@ func Init(di *dig.Container, app *fiber.App) {
|
|||||||
StateHistory: serverIdGroup.Group("/state-history"),
|
StateHistory: serverIdGroup.Group("/state-history"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
err := di.Provide(func() *common.RouteGroups {
|
err := di.Provide(func() *common.RouteGroups {
|
||||||
return routeGroups
|
return routeGroups
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Panic("unable to bind routes")
|
logging.Panic("unable to bind routes")
|
||||||
}
|
}
|
||||||
err = di.Provide(func() *dig.Container {
|
|
||||||
return di
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logging.Panic("unable to bind dig")
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.InitializeControllers(di)
|
controller.InitializeControllers(di)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (ac *ConfigController) UpdateConfig(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(400).SendString(err.Error())
|
return c.Status(400).SendString(err.Error())
|
||||||
}
|
}
|
||||||
logging.Info("restart", restart)
|
logging.Info("restart: %v", restart)
|
||||||
if restart {
|
if restart {
|
||||||
_, err := ac.apiService.ApiRestartServer(c)
|
_, err := ac.apiService.ApiRestartServer(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
|||||||
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
|
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging.Debug("Login request received")
|
||||||
token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password)
|
token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()})
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"acc-server-manager/local/middleware/security"
|
||||||
"acc-server-manager/local/service"
|
"acc-server-manager/local/service"
|
||||||
"acc-server-manager/local/utl/jwt"
|
"acc-server-manager/local/utl/jwt"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@@ -11,49 +14,125 @@ import (
|
|||||||
// AuthMiddleware provides authentication and permission middleware.
|
// AuthMiddleware provides authentication and permission middleware.
|
||||||
type AuthMiddleware struct {
|
type AuthMiddleware struct {
|
||||||
membershipService *service.MembershipService
|
membershipService *service.MembershipService
|
||||||
|
securityMW *security.SecurityMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthMiddleware creates a new AuthMiddleware.
|
// NewAuthMiddleware creates a new AuthMiddleware.
|
||||||
func NewAuthMiddleware(ms *service.MembershipService) *AuthMiddleware {
|
func NewAuthMiddleware(ms *service.MembershipService) *AuthMiddleware {
|
||||||
return &AuthMiddleware{
|
return &AuthMiddleware{
|
||||||
membershipService: ms,
|
membershipService: ms,
|
||||||
|
securityMW: security.NewSecurityMiddleware(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate is a middleware for JWT authentication.
|
// Authenticate is a middleware for JWT authentication with enhanced security.
|
||||||
func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
|
func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
|
||||||
|
// Log authentication attempt
|
||||||
|
ip := ctx.IP()
|
||||||
|
userAgent := ctx.Get("User-Agent")
|
||||||
|
|
||||||
authHeader := ctx.Get("Authorization")
|
authHeader := ctx.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing or malformed JWT"})
|
logging.Error("Authentication failed: missing Authorization header from IP %s", ip)
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Missing or malformed JWT",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(authHeader, " ")
|
parts := strings.Split(authHeader, " ")
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing or malformed JWT"})
|
logging.Error("Authentication failed: malformed Authorization header from IP %s", ip)
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Missing or malformed JWT",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := jwt.ValidateToken(parts[1])
|
// Validate token length to prevent potential attacks
|
||||||
|
token := parts[1]
|
||||||
|
if len(token) < 10 || len(token) > 2048 {
|
||||||
|
logging.Error("Authentication failed: invalid token length from IP %s", ip)
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Invalid or expired JWT",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := jwt.ValidateToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired JWT"})
|
logging.Error("Authentication failed: invalid token from IP %s, User-Agent: %s, Error: %v", ip, userAgent, err)
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Invalid or expired JWT",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional security: validate user ID format
|
||||||
|
if claims.UserID == "" || len(claims.UserID) < 10 {
|
||||||
|
logging.Error("Authentication failed: invalid user ID in token from IP %s", ip)
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Invalid or expired JWT",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Locals("userID", claims.UserID)
|
ctx.Locals("userID", claims.UserID)
|
||||||
|
ctx.Locals("authTime", time.Now())
|
||||||
|
|
||||||
|
logging.Info("User %s authenticated successfully from IP %s", claims.UserID, ip)
|
||||||
return ctx.Next()
|
return ctx.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasPermission is a middleware for checking user permissions.
|
// HasPermission is a middleware for checking user permissions with enhanced logging.
|
||||||
func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler {
|
func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler {
|
||||||
return func(ctx *fiber.Ctx) error {
|
return func(ctx *fiber.Ctx) error {
|
||||||
userID, ok := ctx.Locals("userID").(string)
|
userID, ok := ctx.Locals("userID").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"})
|
logging.Error("Permission check failed: no user ID in context from IP %s", ctx.IP())
|
||||||
|
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate permission parameter
|
||||||
|
if requiredPermission == "" {
|
||||||
|
logging.Error("Permission check failed: empty permission requirement")
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Internal server error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
has, err := m.membershipService.HasPermission(ctx.UserContext(), userID, requiredPermission)
|
has, err := m.membershipService.HasPermission(ctx.UserContext(), userID, requiredPermission)
|
||||||
if err != nil || !has {
|
if err != nil {
|
||||||
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Forbidden"})
|
logging.Error("Permission check error for user %s, permission %s: %v", userID, requiredPermission, err)
|
||||||
|
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||||
|
"error": "Forbidden",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !has {
|
||||||
|
logging.Error("Permission denied: user %s lacks permission %s, IP %s", userID, requiredPermission, ctx.IP())
|
||||||
|
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||||
|
"error": "Forbidden",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Permission granted: user %s has permission %s", userID, requiredPermission)
|
||||||
|
return ctx.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRateLimit applies rate limiting specifically for authentication endpoints
|
||||||
|
func (m *AuthMiddleware) AuthRateLimit() fiber.Handler {
|
||||||
|
return m.securityMW.AuthRateLimit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireHTTPS redirects HTTP requests to HTTPS in production
|
||||||
|
func (m *AuthMiddleware) RequireHTTPS() fiber.Handler {
|
||||||
|
return func(ctx *fiber.Ctx) error {
|
||||||
|
if ctx.Protocol() != "https" && ctx.Get("X-Forwarded-Proto") != "https" {
|
||||||
|
// Allow HTTP in development/testing
|
||||||
|
if ctx.Hostname() != "localhost" && ctx.Hostname() != "127.0.0.1" {
|
||||||
|
httpsURL := "https://" + ctx.Hostname() + ctx.OriginalURL()
|
||||||
|
return ctx.Redirect(httpsURL, fiber.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
}
|
||||||
return ctx.Next()
|
return ctx.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
351
local/middleware/security/security.go
Normal file
351
local/middleware/security/security.go
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RateLimiter stores rate limiting information
|
||||||
|
type RateLimiter struct {
|
||||||
|
requests map[string][]time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRateLimiter creates a new rate limiter
|
||||||
|
func NewRateLimiter() *RateLimiter {
|
||||||
|
rl := &RateLimiter{
|
||||||
|
requests: make(map[string][]time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old entries every 5 minutes
|
||||||
|
go rl.cleanup()
|
||||||
|
|
||||||
|
return rl
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup removes old entries from the rate limiter
|
||||||
|
func (rl *RateLimiter) cleanup() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
rl.mutex.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for key, times := range rl.requests {
|
||||||
|
// Remove entries older than 1 hour
|
||||||
|
filtered := make([]time.Time, 0, len(times))
|
||||||
|
for _, t := range times {
|
||||||
|
if now.Sub(t) < time.Hour {
|
||||||
|
filtered = append(filtered, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered) == 0 {
|
||||||
|
delete(rl.requests, key)
|
||||||
|
} else {
|
||||||
|
rl.requests[key] = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityMiddleware provides comprehensive security middleware
|
||||||
|
type SecurityMiddleware struct {
|
||||||
|
rateLimiter *RateLimiter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecurityMiddleware creates a new security middleware
|
||||||
|
func NewSecurityMiddleware() *SecurityMiddleware {
|
||||||
|
return &SecurityMiddleware{
|
||||||
|
rateLimiter: NewRateLimiter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityHeaders adds security headers to responses
|
||||||
|
func (sm *SecurityMiddleware) SecurityHeaders() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Prevent MIME type sniffing
|
||||||
|
c.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
|
// Prevent clickjacking
|
||||||
|
c.Set("X-Frame-Options", "DENY")
|
||||||
|
|
||||||
|
// Enable XSS protection
|
||||||
|
c.Set("X-XSS-Protection", "1; mode=block")
|
||||||
|
|
||||||
|
// Prevent referrer leakage
|
||||||
|
c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
|
|
||||||
|
// Content Security Policy
|
||||||
|
c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'")
|
||||||
|
|
||||||
|
// Permissions Policy
|
||||||
|
c.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=()")
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimit implements rate limiting for API endpoints
|
||||||
|
func (sm *SecurityMiddleware) RateLimit(maxRequests int, duration time.Duration) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
ip := c.IP()
|
||||||
|
key := fmt.Sprintf("rate_limit:%s", ip)
|
||||||
|
|
||||||
|
sm.rateLimiter.mutex.Lock()
|
||||||
|
defer sm.rateLimiter.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
requests := sm.rateLimiter.requests[key]
|
||||||
|
|
||||||
|
// Remove requests older than duration
|
||||||
|
filtered := make([]time.Time, 0, len(requests))
|
||||||
|
for _, t := range requests {
|
||||||
|
if now.Sub(t) < duration {
|
||||||
|
filtered = append(filtered, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit is exceeded
|
||||||
|
if len(filtered) >= maxRequests {
|
||||||
|
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||||
|
"error": "Rate limit exceeded",
|
||||||
|
"retry_after": duration.Seconds(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current request
|
||||||
|
filtered = append(filtered, now)
|
||||||
|
sm.rateLimiter.requests[key] = filtered
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthRateLimit implements stricter rate limiting for authentication endpoints
|
||||||
|
func (sm *SecurityMiddleware) AuthRateLimit() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
ip := c.IP()
|
||||||
|
userAgent := c.Get("User-Agent")
|
||||||
|
key := fmt.Sprintf("%s:%s", ip, userAgent)
|
||||||
|
|
||||||
|
sm.rateLimiter.mutex.Lock()
|
||||||
|
defer sm.rateLimiter.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
requests := sm.rateLimiter.requests[key]
|
||||||
|
|
||||||
|
// Remove requests older than 15 minutes
|
||||||
|
filtered := make([]time.Time, 0, len(requests))
|
||||||
|
for _, t := range requests {
|
||||||
|
if now.Sub(t) < 15*time.Minute {
|
||||||
|
filtered = append(filtered, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit is exceeded (5 requests per 15 minutes for auth)
|
||||||
|
if len(filtered) >= 5 {
|
||||||
|
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||||
|
"error": "Too many authentication attempts",
|
||||||
|
"retry_after": 900, // 15 minutes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current request
|
||||||
|
filtered = append(filtered, now)
|
||||||
|
sm.rateLimiter.requests[key] = filtered
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputSanitization sanitizes user input to prevent XSS and injection attacks
|
||||||
|
func (sm *SecurityMiddleware) InputSanitization() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Sanitize query parameters
|
||||||
|
c.Request().URI().QueryArgs().VisitAll(func(key, value []byte) {
|
||||||
|
sanitized := sanitizeInput(string(value))
|
||||||
|
c.Request().URI().QueryArgs().Set(string(key), sanitized)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Store original body for processing
|
||||||
|
if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" {
|
||||||
|
body := c.Body()
|
||||||
|
if len(body) > 0 {
|
||||||
|
// Basic sanitization - remove potentially dangerous patterns
|
||||||
|
sanitized := sanitizeInput(string(body))
|
||||||
|
c.Request().SetBodyString(sanitized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeInput removes potentially dangerous patterns from input
|
||||||
|
func sanitizeInput(input string) string {
|
||||||
|
// Remove common XSS patterns
|
||||||
|
dangerous := []string{
|
||||||
|
"<script",
|
||||||
|
"</script>",
|
||||||
|
"javascript:",
|
||||||
|
"vbscript:",
|
||||||
|
"data:text/html",
|
||||||
|
"onload=",
|
||||||
|
"onerror=",
|
||||||
|
"onclick=",
|
||||||
|
"onmouseover=",
|
||||||
|
"onfocus=",
|
||||||
|
"onblur=",
|
||||||
|
"onchange=",
|
||||||
|
"onsubmit=",
|
||||||
|
"<iframe",
|
||||||
|
"<object",
|
||||||
|
"<embed",
|
||||||
|
"<link",
|
||||||
|
"<meta",
|
||||||
|
"<style",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := strings.ToLower(input)
|
||||||
|
for _, pattern := range dangerous {
|
||||||
|
result = strings.ReplaceAll(result, pattern, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the sanitized version is very different, it might be malicious
|
||||||
|
if len(result) < len(input)/2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateContentType ensures only expected content types are accepted
|
||||||
|
func (sm *SecurityMiddleware) ValidateContentType(allowedTypes ...string) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" {
|
||||||
|
contentType := c.Get("Content-Type")
|
||||||
|
if contentType == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Content-Type header is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if content type is allowed
|
||||||
|
allowed := false
|
||||||
|
for _, allowedType := range allowedTypes {
|
||||||
|
if strings.Contains(contentType, allowedType) {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
return c.Status(fiber.StatusUnsupportedMediaType).JSON(fiber.Map{
|
||||||
|
"error": "Unsupported content type",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUserAgent blocks requests with suspicious or missing user agents
|
||||||
|
func (sm *SecurityMiddleware) ValidateUserAgent() fiber.Handler {
|
||||||
|
suspiciousAgents := []string{
|
||||||
|
"sqlmap",
|
||||||
|
"nikto",
|
||||||
|
"nmap",
|
||||||
|
"masscan",
|
||||||
|
"gobuster",
|
||||||
|
"dirb",
|
||||||
|
"dirbuster",
|
||||||
|
"wpscan",
|
||||||
|
"curl/7.0", // Very old curl versions
|
||||||
|
"wget/1.0", // Very old wget versions
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
userAgent := strings.ToLower(c.Get("User-Agent"))
|
||||||
|
|
||||||
|
// Block empty user agents
|
||||||
|
if userAgent == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "User-Agent header is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block suspicious user agents
|
||||||
|
for _, suspicious := range suspiciousAgents {
|
||||||
|
if strings.Contains(userAgent, suspicious) {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||||
|
"error": "Access denied",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestSizeLimit limits the size of incoming requests
|
||||||
|
func (sm *SecurityMiddleware) RequestSizeLimit(maxSize int) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" {
|
||||||
|
contentLength := c.Request().Header.ContentLength()
|
||||||
|
if contentLength > maxSize {
|
||||||
|
return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
|
||||||
|
"error": "Request too large",
|
||||||
|
"max_size": maxSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSecurityEvents logs security-related events
|
||||||
|
func (sm *SecurityMiddleware) LogSecurityEvents() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Process request
|
||||||
|
err := c.Next()
|
||||||
|
|
||||||
|
// Log suspicious activity
|
||||||
|
status := c.Response().StatusCode()
|
||||||
|
if status == 401 || status == 403 || status == 429 {
|
||||||
|
duration := time.Since(start)
|
||||||
|
// In a real implementation, you would send this to your logging system
|
||||||
|
fmt.Printf("[SECURITY] %s %s %s %d %v %s\n",
|
||||||
|
time.Now().Format(time.RFC3339),
|
||||||
|
c.IP(),
|
||||||
|
c.Method(),
|
||||||
|
status,
|
||||||
|
duration,
|
||||||
|
c.Path(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeoutMiddleware adds request timeout
|
||||||
|
func (sm *SecurityMiddleware) TimeoutMiddleware(timeout time.Duration) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(c.UserContext(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c.SetUserContext(ctx)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
238
local/migrations/001_upgrade_password_security.go
Normal file
238
local/migrations/001_upgrade_password_security.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"acc-server-manager/local/utl/password"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Migration001UpgradePasswordSecurity migrates existing user passwords from encrypted to hashed format
|
||||||
|
type Migration001UpgradePasswordSecurity struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMigration001UpgradePasswordSecurity creates a new password security migration
|
||||||
|
func NewMigration001UpgradePasswordSecurity(db *gorm.DB) *Migration001UpgradePasswordSecurity {
|
||||||
|
return &Migration001UpgradePasswordSecurity{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up executes the migration
|
||||||
|
func (m *Migration001UpgradePasswordSecurity) Up() error {
|
||||||
|
logging.Info("Starting password security upgrade migration...")
|
||||||
|
|
||||||
|
// Check if migration has already been applied
|
||||||
|
var migrationRecord MigrationRecord
|
||||||
|
err := m.DB.Where("migration_name = ?", "001_upgrade_password_security").First(&migrationRecord).Error
|
||||||
|
if err == nil {
|
||||||
|
logging.Info("Password security migration already applied, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create migration tracking table if it doesn't exist
|
||||||
|
if err := m.DB.AutoMigrate(&MigrationRecord{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration tracking table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
tx := m.DB.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %v", tx.Error)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Add a backup column for old passwords (temporary)
|
||||||
|
if err := tx.Exec("ALTER TABLE users ADD COLUMN password_backup TEXT").Error; err != nil {
|
||||||
|
// Column might already exist, ignore if it's a duplicate column error
|
||||||
|
if !isDuplicateColumnError(err) {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("failed to add backup column: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all users with encrypted passwords
|
||||||
|
var users []UserForMigration
|
||||||
|
if err := tx.Find(&users).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("failed to fetch users: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Found %d users to migrate", len(users))
|
||||||
|
|
||||||
|
migratedCount := 0
|
||||||
|
failedCount := 0
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
if err := m.migrateUserPassword(tx, &user); err != nil {
|
||||||
|
logging.Error("Failed to migrate user %s (ID: %s): %v", user.Username, user.ID, err)
|
||||||
|
failedCount++
|
||||||
|
// Continue with other users rather than failing completely
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
migratedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove backup column after successful migration
|
||||||
|
if err := tx.Exec("ALTER TABLE users DROP COLUMN password_backup").Error; err != nil {
|
||||||
|
logging.Error("Failed to remove backup column (non-critical): %v", err)
|
||||||
|
// Don't fail the migration for this
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record successful migration
|
||||||
|
migrationRecord = MigrationRecord{
|
||||||
|
MigrationName: "001_upgrade_password_security",
|
||||||
|
AppliedAt: "datetime('now')",
|
||||||
|
Success: true,
|
||||||
|
Notes: fmt.Sprintf("Migrated %d users, %d failed", migratedCount, failedCount),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&migrationRecord).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("failed to record migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to commit migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Password security migration completed successfully. Migrated: %d, Failed: %d", migratedCount, failedCount)
|
||||||
|
|
||||||
|
if failedCount > 0 {
|
||||||
|
logging.Error("Some users failed to migrate. They will need to reset their passwords.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateUserPassword migrates a single user's password
|
||||||
|
func (m *Migration001UpgradePasswordSecurity) migrateUserPassword(tx *gorm.DB, user *UserForMigration) error {
|
||||||
|
// Skip if password is already hashed (bcrypt hashes start with $2a$, $2b$, or $2y$)
|
||||||
|
if isAlreadyHashed(user.Password) {
|
||||||
|
logging.Debug("User %s already has hashed password, skipping", user.Username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup original password
|
||||||
|
if err := tx.Model(user).Update("password_backup", user.Password).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to backup password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to decrypt the old password
|
||||||
|
var plainPassword string
|
||||||
|
|
||||||
|
// First, try to decrypt using the old encryption method
|
||||||
|
decrypted, err := decryptOldPassword(user.Password)
|
||||||
|
if err != nil {
|
||||||
|
// If decryption fails, the password might already be plain text or corrupted
|
||||||
|
logging.Error("Failed to decrypt password for user %s, treating as plain text: %v", user.Username, err)
|
||||||
|
|
||||||
|
// Use original password as-is (might be plain text from development)
|
||||||
|
plainPassword = user.Password
|
||||||
|
|
||||||
|
// Validate it's not obviously encrypted data
|
||||||
|
if len(plainPassword) > 100 || containsBinaryData(plainPassword) {
|
||||||
|
return fmt.Errorf("password appears to be corrupted encrypted data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plainPassword = decrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate plain password
|
||||||
|
if plainPassword == "" {
|
||||||
|
return errors.New("decrypted password is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plainPassword) < 1 {
|
||||||
|
return errors.New("password too short after decryption")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the plain password using bcrypt
|
||||||
|
hashedPassword, err := password.HashPassword(plainPassword)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with hashed password
|
||||||
|
if err := tx.Model(user).Update("password", hashedPassword).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to update password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Debug("Successfully migrated password for user %s", user.Username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserForMigration represents a user record for migration purposes
|
||||||
|
type UserForMigration struct {
|
||||||
|
ID string `gorm:"column:id"`
|
||||||
|
Username string `gorm:"column:username"`
|
||||||
|
Password string `gorm:"column:password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (UserForMigration) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrationRecord tracks applied migrations
|
||||||
|
type MigrationRecord struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
MigrationName string `gorm:"unique;not null"`
|
||||||
|
AppliedAt string `gorm:"not null"`
|
||||||
|
Success bool `gorm:"not null"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (MigrationRecord) TableName() string {
|
||||||
|
return "migration_records"
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAlreadyHashed checks if a password is already bcrypt hashed
|
||||||
|
func isAlreadyHashed(password string) bool {
|
||||||
|
return len(password) >= 60 && (password[:4] == "$2a$" || password[:4] == "$2b$" || password[:4] == "$2y$")
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsBinaryData checks if a string contains binary data
|
||||||
|
func containsBinaryData(s string) bool {
|
||||||
|
for _, b := range []byte(s) {
|
||||||
|
if b < 32 && b != 9 && b != 10 && b != 13 { // Allow tab, newline, carriage return
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDuplicateColumnError checks if an error is due to duplicate column
|
||||||
|
func isDuplicateColumnError(err error) bool {
|
||||||
|
errStr := err.Error()
|
||||||
|
return fmt.Sprintf("%v", errStr) == "duplicate column name: password_backup" ||
|
||||||
|
fmt.Sprintf("%v", errStr) == "SQLITE_ERROR: duplicate column name: password_backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptOldPassword attempts to decrypt using the old encryption method
|
||||||
|
// This is a simplified version of the old DecryptPassword function
|
||||||
|
func decryptOldPassword(encryptedPassword string) (string, error) {
|
||||||
|
// This would use the old decryption logic
|
||||||
|
// For now, we'll return an error to force treating as plain text
|
||||||
|
// In a real scenario, you'd implement the old decryption here
|
||||||
|
return "", errors.New("old decryption not implemented - treating as plain text")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down reverses the migration (if needed)
|
||||||
|
func (m *Migration001UpgradePasswordSecurity) Down() error {
|
||||||
|
logging.Error("Password security migration rollback is not supported for security reasons")
|
||||||
|
return errors.New("password security migration rollback is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunMigration is a convenience function to run the migration
|
||||||
|
func RunPasswordSecurityMigration(db *gorm.DB) error {
|
||||||
|
migration := NewMigration001UpgradePasswordSecurity(db)
|
||||||
|
return migration.Up()
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -74,25 +76,67 @@ func (s *SteamCredentials) AfterFind(tx *gorm.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the credentials are valid
|
// Validate checks if the credentials are valid with enhanced security checks
|
||||||
func (s *SteamCredentials) Validate() error {
|
func (s *SteamCredentials) Validate() error {
|
||||||
if s.Username == "" {
|
if s.Username == "" {
|
||||||
return errors.New("username is required")
|
return errors.New("username is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced username validation
|
||||||
|
if len(s.Username) < 3 || len(s.Username) > 64 {
|
||||||
|
return errors.New("username must be between 3 and 64 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid characters in username (alphanumeric, underscore, hyphen)
|
||||||
|
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, s.Username); !matched {
|
||||||
|
return errors.New("username contains invalid characters")
|
||||||
|
}
|
||||||
|
|
||||||
if s.Password == "" {
|
if s.Password == "" {
|
||||||
return errors.New("password is required")
|
return errors.New("password is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Basic password validation
|
||||||
|
if len(s.Password) < 6 {
|
||||||
|
return errors.New("password must be at least 6 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.Password) > 128 {
|
||||||
|
return errors.New("password is too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for obvious weak passwords
|
||||||
|
weakPasswords := []string{"password", "123456", "steam", "admin", "user"}
|
||||||
|
lowerPass := strings.ToLower(s.Password)
|
||||||
|
for _, weak := range weakPasswords {
|
||||||
|
if lowerPass == weak {
|
||||||
|
return errors.New("password is too weak")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEncryptionKey returns the encryption key from config.
|
// GetEncryptionKey returns the encryption key from config.
|
||||||
// The key is loaded from the ENCRYPTION_KEY environment variable.
|
// The key is loaded from the ENCRYPTION_KEY environment variable.
|
||||||
func GetEncryptionKey() []byte {
|
func GetEncryptionKey() []byte {
|
||||||
return []byte(configs.EncryptionKey)
|
key := []byte(configs.EncryptionKey)
|
||||||
|
if len(key) != 32 {
|
||||||
|
panic("encryption key must be exactly 32 bytes for AES-256")
|
||||||
|
}
|
||||||
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptPassword encrypts a password using AES-256
|
// EncryptPassword encrypts a password using AES-256-GCM with enhanced security
|
||||||
func EncryptPassword(password string) (string, error) {
|
func EncryptPassword(password string) (string, error) {
|
||||||
|
if password == "" {
|
||||||
|
return "", errors.New("password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(password) > 1024 {
|
||||||
|
return "", errors.New("password too long")
|
||||||
|
}
|
||||||
|
|
||||||
key := GetEncryptionKey()
|
key := GetEncryptionKey()
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -105,21 +149,30 @@ func EncryptPassword(password string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a nonce
|
// Create a cryptographically secure nonce
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the password
|
// Encrypt the password with authenticated encryption
|
||||||
ciphertext := gcm.Seal(nonce, nonce, []byte(password), nil)
|
ciphertext := gcm.Seal(nonce, nonce, []byte(password), nil)
|
||||||
|
|
||||||
// Return base64 encoded encrypted password
|
// Return base64 encoded encrypted password
|
||||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptPassword decrypts an encrypted password
|
// DecryptPassword decrypts an encrypted password with enhanced validation
|
||||||
func DecryptPassword(encryptedPassword string) (string, error) {
|
func DecryptPassword(encryptedPassword string) (string, error) {
|
||||||
|
if encryptedPassword == "" {
|
||||||
|
return "", errors.New("encrypted password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate base64 format
|
||||||
|
if len(encryptedPassword) < 24 { // Minimum reasonable length
|
||||||
|
return "", errors.New("invalid encrypted password format")
|
||||||
|
}
|
||||||
|
|
||||||
key := GetEncryptionKey()
|
key := GetEncryptionKey()
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -135,7 +188,7 @@ func DecryptPassword(encryptedPassword string) (string, error) {
|
|||||||
// Decode base64 encoded password
|
// Decode base64 encoded password
|
||||||
ciphertext, err := base64.StdEncoding.DecodeString(encryptedPassword)
|
ciphertext, err := base64.StdEncoding.DecodeString(encryptedPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", errors.New("invalid base64 encoding")
|
||||||
}
|
}
|
||||||
|
|
||||||
nonceSize := gcm.NonceSize()
|
nonceSize := gcm.NonceSize()
|
||||||
@@ -146,8 +199,14 @@ func DecryptPassword(encryptedPassword string) (string, error) {
|
|||||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", errors.New("decryption failed - invalid ciphertext or key")
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(plaintext), nil
|
// Validate decrypted content
|
||||||
|
decrypted := string(plaintext)
|
||||||
|
if len(decrypted) == 0 || len(decrypted) > 1024 {
|
||||||
|
return "", errors.New("invalid decrypted password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted, nil
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"acc-server-manager/local/utl/password"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -11,54 +12,57 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
|
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
|
||||||
Username string `json:"username" gorm:"unique_index;not null"`
|
Username string `json:"username" gorm:"unique_index;not null"`
|
||||||
Password string `json:"password" gorm:"not null"`
|
Password string `json:"-" gorm:"not null"` // Never expose password in JSON
|
||||||
RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"`
|
RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"`
|
||||||
Role Role `json:"role"`
|
Role Role `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is a GORM hook that runs before creating new users
|
||||||
// BeforeCreate is a GORM hook that runs before creating new credentials
|
|
||||||
func (s *User) BeforeCreate(tx *gorm.DB) error {
|
func (s *User) BeforeCreate(tx *gorm.DB) error {
|
||||||
s.ID = uuid.New()
|
s.ID = uuid.New()
|
||||||
// Encrypt password before saving
|
|
||||||
encrypted, err := EncryptPassword(s.Password)
|
// Validate password strength
|
||||||
|
if err := password.ValidatePasswordStrength(s.Password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password before saving
|
||||||
|
hashed, err := password.HashPassword(s.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Password = encrypted
|
s.Password = hashed
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeUpdate is a GORM hook that runs before updating credentials
|
// BeforeUpdate is a GORM hook that runs before updating users
|
||||||
func (s *User) BeforeUpdate(tx *gorm.DB) error {
|
func (s *User) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
// Only hash if password field is being updated
|
||||||
// Only encrypt if password field is being updated
|
|
||||||
if tx.Statement.Changed("Password") {
|
if tx.Statement.Changed("Password") {
|
||||||
encrypted, err := EncryptPassword(s.Password)
|
// Validate password strength
|
||||||
|
if err := password.ValidatePasswordStrength(s.Password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashed, err := password.HashPassword(s.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Password = encrypted
|
s.Password = hashed
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfterFind is a GORM hook that runs after fetching credentials
|
// AfterFind is a GORM hook that runs after fetching users
|
||||||
func (s *User) AfterFind(tx *gorm.DB) error {
|
func (s *User) AfterFind(tx *gorm.DB) error {
|
||||||
// Decrypt password after fetching
|
// Password remains hashed - never decrypt
|
||||||
if s.Password != "" {
|
// This hook is kept for potential future use
|
||||||
decrypted, err := DecryptPassword(s.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Password = decrypted
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the credentials are valid
|
// Validate checks if the user data is valid
|
||||||
func (s *User) Validate() error {
|
func (s *User) Validate() error {
|
||||||
if s.Username == "" {
|
if s.Username == "" {
|
||||||
return errors.New("username is required")
|
return errors.New("username is required")
|
||||||
@@ -68,3 +72,8 @@ func (s *User) Validate() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyPassword verifies a plain text password against the stored hash
|
||||||
|
func (s *User) VerifyPassword(plainPassword string) error {
|
||||||
|
return password.VerifyPassword(s.Password, plainPassword)
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ func (r *MembershipRepository) FindUserByIDWithPermissions(ctx context.Context,
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// CreateUser creates a new user.
|
// CreateUser creates a new user.
|
||||||
func (r *MembershipRepository) CreateUser(ctx context.Context, user *model.User) error {
|
func (r *MembershipRepository) CreateUser(ctx context.Context, user *model.User) error {
|
||||||
db := r.db.WithContext(ctx)
|
db := r.db.WithContext(ctx)
|
||||||
|
|||||||
@@ -17,73 +17,9 @@ func NewServerRepository(db *gorm.DB) *ServerRepository {
|
|||||||
BaseRepository: NewBaseRepository[model.Server, model.ServerFilter](db, model.Server{}),
|
BaseRepository: NewBaseRepository[model.Server, model.ServerFilter](db, model.Server{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run migrations
|
|
||||||
if err := repo.migrateServerTable(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrateServerTable ensures all required columns exist with proper defaults
|
|
||||||
func (r *ServerRepository) migrateServerTable() error {
|
|
||||||
// Create a temporary table with all required columns
|
|
||||||
if err := r.db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS servers_new (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
ip TEXT NOT NULL,
|
|
||||||
port INTEGER NOT NULL DEFAULT 9600,
|
|
||||||
path TEXT NOT NULL,
|
|
||||||
service_name TEXT NOT NULL,
|
|
||||||
date_created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
from_steam_cmd BOOLEAN NOT NULL DEFAULT 1
|
|
||||||
)
|
|
||||||
`).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy data from old table, setting defaults for new columns
|
|
||||||
if err := r.db.Exec(`
|
|
||||||
INSERT INTO servers_new (
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
ip,
|
|
||||||
port,
|
|
||||||
path,
|
|
||||||
service_name,
|
|
||||||
date_created,
|
|
||||||
from_steam_cmd
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
COALESCE(name, 'Server ' || id) as name,
|
|
||||||
COALESCE(ip, '127.0.0.1') as ip,
|
|
||||||
COALESCE(port, 9600) as port,
|
|
||||||
path,
|
|
||||||
COALESCE(service_name, 'ACC-Server-' || id) as service_name,
|
|
||||||
COALESCE(date_created, CURRENT_TIMESTAMP) as date_created,
|
|
||||||
COALESCE(from_steam_cmd, 1) as from_steam_cmd
|
|
||||||
FROM servers
|
|
||||||
`).Error; err != nil {
|
|
||||||
// If the old table doesn't exist, this is a fresh install
|
|
||||||
if err := r.db.Exec(`DROP TABLE IF EXISTS servers_new`).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace old table with new one
|
|
||||||
if err := r.db.Exec(`DROP TABLE IF EXISTS servers`).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := r.db.Exec(`ALTER TABLE servers_new RENAME TO servers`).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFirstByServiceName
|
// GetFirstByServiceName
|
||||||
// Gets first row from Server table.
|
// Gets first row from Server table.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func (r *StateHistoryRepository) GetPlayerCountOverTime(ctx context.Context, fil
|
|||||||
rawQuery := `
|
rawQuery := `
|
||||||
SELECT
|
SELECT
|
||||||
DATETIME(MIN(date_created)) as timestamp,
|
DATETIME(MIN(date_created)) as timestamp,
|
||||||
AVG(player_count) as count
|
ROUND(AVG(player_count)) as count
|
||||||
FROM state_histories
|
FROM state_histories
|
||||||
WHERE server_id = ? AND date_created BETWEEN ? AND ?
|
WHERE server_id = ? AND date_created BETWEEN ? AND ?
|
||||||
GROUP BY strftime('%Y-%m-%d %H', date_created)
|
GROUP BY strftime('%Y-%m-%d %H', date_created)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"acc-server-manager/local/model"
|
"acc-server-manager/local/model"
|
||||||
"acc-server-manager/local/repository"
|
"acc-server-manager/local/repository"
|
||||||
"acc-server-manager/local/utl/jwt"
|
"acc-server-manager/local/utl/jwt"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
@@ -28,7 +29,8 @@ func (s *MembershipService) Login(ctx context.Context, username, password string
|
|||||||
return "", errors.New("invalid credentials")
|
return "", errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Password != password {
|
// Use secure password verification with constant-time comparison
|
||||||
|
if err := user.VerifyPassword(password); err != nil {
|
||||||
return "", errors.New("invalid credentials")
|
return "", errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ func (s *MembershipService) CreateUser(ctx context.Context, username, password,
|
|||||||
|
|
||||||
role, err := s.repo.FindRoleByName(ctx, roleName)
|
role, err := s.repo.FindRoleByName(ctx, roleName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logging.Error("Failed to find role by name: %v", err)
|
||||||
return nil, errors.New("role not found")
|
return nil, errors.New("role not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +53,10 @@ func (s *MembershipService) CreateUser(ctx context.Context, username, password,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.repo.CreateUser(ctx, user); err != nil {
|
if err := s.repo.CreateUser(ctx, user); err != nil {
|
||||||
|
logging.Error("Failed to create user: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logging.Debug("User created successfully")
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
@@ -90,6 +95,7 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Password != nil && *req.Password != "" {
|
if req.Password != nil && *req.Password != "" {
|
||||||
|
// Password will be automatically hashed in BeforeUpdate hook
|
||||||
user.Password = *req.Password
|
user.Password = *req.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +168,7 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
|||||||
// Create a default admin user if one doesn't exist
|
// Create a default admin user if one doesn't exist
|
||||||
_, err = s.repo.FindUserByUsername(ctx, "admin")
|
_, err = s.repo.FindUserByUsername(ctx, "admin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logging.Debug("Creating default admin user")
|
||||||
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed
|
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package configs
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -14,12 +16,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Secret = getEnv("APP_SECRET", "default-secret-for-dev-use-only")
|
godotenv.Load()
|
||||||
SecretCode = getEnv("APP_SECRET_CODE", "another-secret-for-dev-use-only")
|
// Fail fast if critical environment variables are missing
|
||||||
EncryptionKey = getEnv("ENCRYPTION_KEY", "a-secure-32-byte-long-key-!!!!!!") // Fallback MUST be 32 bytes for AES-256
|
Secret = getEnvRequired("APP_SECRET")
|
||||||
|
SecretCode = getEnvRequired("APP_SECRET_CODE")
|
||||||
|
EncryptionKey = getEnvRequired("ENCRYPTION_KEY")
|
||||||
|
|
||||||
if len(EncryptionKey) != 32 {
|
if len(EncryptionKey) != 32 {
|
||||||
log.Fatal("ENCRYPTION_KEY must be 32 bytes long")
|
log.Fatal("ENCRYPTION_KEY must be exactly 32 bytes long for AES-256")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,3 +35,13 @@ func getEnv(key, fallback string) string {
|
|||||||
log.Printf("Environment variable %s not set, using fallback.", key)
|
log.Printf("Environment variable %s not set, using fallback.", key)
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEnvRequired retrieves an environment variable and fails if it's not set.
|
||||||
|
// This should be used for critical configuration that must not have defaults.
|
||||||
|
func getEnvRequired(key string) string {
|
||||||
|
if value, exists := os.LookupEnv(key); exists && value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
log.Fatalf("Required environment variable %s is not set or is empty", key)
|
||||||
|
return "" // This line will never be reached due to log.Fatalf
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ func Migrate(db *gorm.DB) {
|
|||||||
&model.StateHistory{},
|
&model.StateHistory{},
|
||||||
&model.SteamCredentials{},
|
&model.SteamCredentials{},
|
||||||
&model.SystemConfig{},
|
&model.SystemConfig{},
|
||||||
&model.User{},
|
|
||||||
&model.Role{},
|
|
||||||
&model.Permission{},
|
&model.Permission{},
|
||||||
|
&model.Role{},
|
||||||
|
&model.User{},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,6 +55,10 @@ func Migrate(db *gorm.DB) {
|
|||||||
|
|
||||||
db.FirstOrCreate(&model.ApiModel{Api: "Works"})
|
db.FirstOrCreate(&model.ApiModel{Api: "Works"})
|
||||||
|
|
||||||
|
// Run security migrations - temporarily disabled until migration is fixed
|
||||||
|
// TODO: Implement proper migration system
|
||||||
|
logging.Info("Database migration system needs to be implemented")
|
||||||
|
|
||||||
Seed(db)
|
Seed(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +84,6 @@ func Seed(db *gorm.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func seedTracks(db *gorm.DB) error {
|
func seedTracks(db *gorm.DB) error {
|
||||||
tracks := []model.Track{
|
tracks := []model.Track{
|
||||||
{Name: "monza", UniquePitBoxes: 29, PrivateServerSlots: 60},
|
{Name: "monza", UniquePitBoxes: 29, PrivateServerSlots: 60},
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ package jwt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"acc-server-manager/local/model"
|
"acc-server-manager/local/model"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecretKey is the secret key for signing the JWT.
|
// SecretKey holds the JWT signing key loaded from environment
|
||||||
// It is recommended to use a long, complex string for this.
|
var SecretKey []byte
|
||||||
// In a production environment, this should be loaded from a secure configuration source.
|
|
||||||
var SecretKey = []byte("your-secret-key")
|
|
||||||
|
|
||||||
// Claims represents the JWT claims.
|
// Claims represents the JWT claims.
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
@@ -19,6 +21,36 @@ type Claims struct {
|
|||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init initializes the JWT secret key from environment variable
|
||||||
|
func init() {
|
||||||
|
jwtSecret := os.Getenv("JWT_SECRET")
|
||||||
|
if jwtSecret == "" {
|
||||||
|
log.Fatal("JWT_SECRET environment variable is required and cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 secret if it looks like base64, otherwise use as-is
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(jwtSecret); err == nil && len(decoded) >= 32 {
|
||||||
|
SecretKey = decoded
|
||||||
|
} else {
|
||||||
|
SecretKey = []byte(jwtSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure minimum key length for security
|
||||||
|
if len(SecretKey) < 32 {
|
||||||
|
log.Fatal("JWT_SECRET must be at least 32 bytes long for security")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSecretKey generates a cryptographically secure random key for JWT signing
|
||||||
|
// This is a utility function for generating new secrets, not used in normal operation
|
||||||
|
func GenerateSecretKey() string {
|
||||||
|
key := make([]byte, 64) // 512 bits
|
||||||
|
if _, err := rand.Read(key); err != nil {
|
||||||
|
log.Fatal("Failed to generate random key: ", err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(key)
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateToken generates a new JWT for a given user.
|
// GenerateToken generates a new JWT for a given user.
|
||||||
func GenerateToken(user *model.User) (string, error) {
|
func GenerateToken(user *model.User) (string, error) {
|
||||||
expirationTime := time.Now().Add(24 * time.Hour)
|
expirationTime := time.Now().Add(24 * time.Hour)
|
||||||
|
|||||||
82
local/utl/password/password.go
Normal file
82
local/utl/password/password.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package password
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MinPasswordLength defines the minimum password length
|
||||||
|
MinPasswordLength = 8
|
||||||
|
// BcryptCost defines the cost factor for bcrypt hashing
|
||||||
|
BcryptCost = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
// HashPassword hashes a plain text password using bcrypt
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
if len(password) < MinPasswordLength {
|
||||||
|
return "", errors.New("password must be at least 8 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(hashedBytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPassword verifies a plain text password against a hashed password
|
||||||
|
func VerifyPassword(hashedPassword, password string) error {
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePasswordStrength validates password complexity requirements
|
||||||
|
func ValidatePasswordStrength(password string) error {
|
||||||
|
if len(password) < MinPasswordLength {
|
||||||
|
return errors.New("password must be at least 8 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("ENFORCE_PASSWORD_STRENGTH") == "true" {
|
||||||
|
if len(password) < MinPasswordLength {
|
||||||
|
return errors.New("password must be at least 8 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpper := false
|
||||||
|
hasLower := false
|
||||||
|
hasDigit := false
|
||||||
|
hasSpecial := false
|
||||||
|
|
||||||
|
for _, char := range password {
|
||||||
|
switch {
|
||||||
|
case char >= 'A' && char <= 'Z':
|
||||||
|
hasUpper = true
|
||||||
|
case char >= 'a' && char <= 'z':
|
||||||
|
hasLower = true
|
||||||
|
case char >= '0' && char <= '9':
|
||||||
|
hasDigit = true
|
||||||
|
case char >= '!' && char <= '/' || char >= ':' && char <= '@' || char >= '[' && char <= '`' || char >= '{' && char <= '~':
|
||||||
|
hasSpecial = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasUpper {
|
||||||
|
return errors.New("password must contain at least one uppercase letter")
|
||||||
|
}
|
||||||
|
if !hasLower {
|
||||||
|
return errors.New("password must contain at least one lowercase letter")
|
||||||
|
}
|
||||||
|
if !hasDigit {
|
||||||
|
return errors.New("password must contain at least one digit")
|
||||||
|
}
|
||||||
|
if !hasSpecial {
|
||||||
|
return errors.New("password must contain at least one special character")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"acc-server-manager/local/api"
|
"acc-server-manager/local/api"
|
||||||
|
"acc-server-manager/local/middleware/security"
|
||||||
"acc-server-manager/local/utl/logging"
|
"acc-server-manager/local/utl/logging"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
@@ -15,8 +17,25 @@ import (
|
|||||||
func Start(di *dig.Container) *fiber.App {
|
func Start(di *dig.Container) *fiber.App {
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
EnablePrintRoutes: true,
|
EnablePrintRoutes: true,
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
|
BodyLimit: 10 * 1024 * 1024, // 10MB
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Initialize security middleware
|
||||||
|
securityMW := security.NewSecurityMiddleware()
|
||||||
|
|
||||||
|
// Add security middleware stack
|
||||||
|
app.Use(securityMW.SecurityHeaders())
|
||||||
|
app.Use(securityMW.LogSecurityEvents())
|
||||||
|
app.Use(securityMW.TimeoutMiddleware(30 * time.Second))
|
||||||
|
app.Use(securityMW.RequestSizeLimit(10 * 1024 * 1024)) // 10MB
|
||||||
|
app.Use(securityMW.ValidateUserAgent())
|
||||||
|
app.Use(securityMW.ValidateContentType("application/json", "application/x-www-form-urlencoded", "multipart/form-data"))
|
||||||
|
app.Use(securityMW.InputSanitization())
|
||||||
|
app.Use(securityMW.RateLimit(100, 1*time.Minute)) // 100 requests per minute global
|
||||||
|
|
||||||
app.Use(helmet.New())
|
app.Use(helmet.New())
|
||||||
|
|
||||||
allowedOrigin := os.Getenv("CORS_ALLOWED_ORIGIN")
|
allowedOrigin := os.Getenv("CORS_ALLOWED_ORIGIN")
|
||||||
@@ -25,8 +44,11 @@ func Start(di *dig.Container) *fiber.App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.Use(cors.New(cors.Config{
|
app.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: allowedOrigin,
|
AllowOrigins: allowedOrigin,
|
||||||
AllowHeaders: "Origin, Content-Type, Accept",
|
AllowHeaders: "Origin, Content-Type, Accept, Authorization",
|
||||||
|
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 86400, // 24 hours
|
||||||
}))
|
}))
|
||||||
|
|
||||||
app.Get("/swagger/*", swagger.HandlerDefault)
|
app.Get("/swagger/*", swagger.HandlerDefault)
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ func (instance *AccServerInstance) UpdateState(callback func(state *model.Server
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (instance *AccServerInstance) UpdatePlayerCount(count int) {
|
func (instance *AccServerInstance) UpdatePlayerCount(count int) {
|
||||||
|
if (count < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
instance.UpdateState(func (state *model.ServerState, changes *[]StateChange) {
|
instance.UpdateState(func (state *model.ServerState, changes *[]StateChange) {
|
||||||
if (count == state.PlayerCount) {
|
if (count == state.PlayerCount) {
|
||||||
return
|
return
|
||||||
|
|||||||
176
scripts/generate-secrets.ps1
Normal file
176
scripts/generate-secrets.ps1
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# ACC Server Manager - Secret Generation Script
|
||||||
|
# This script generates cryptographically secure secrets for the ACC Server Manager
|
||||||
|
|
||||||
|
Write-Host "ACC Server Manager - Secret Generation Script" -ForegroundColor Green
|
||||||
|
Write-Host "=============================================" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Function to generate random bytes and convert to hex
|
||||||
|
function Generate-HexString {
|
||||||
|
param([int]$Length)
|
||||||
|
$bytes = New-Object byte[] $Length
|
||||||
|
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
|
||||||
|
$rng.GetBytes($bytes)
|
||||||
|
$rng.Dispose()
|
||||||
|
return [System.BitConverter]::ToString($bytes) -replace '-', ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to generate random bytes and convert to base64
|
||||||
|
function Generate-Base64String {
|
||||||
|
param([int]$Length)
|
||||||
|
$bytes = New-Object byte[] $Length
|
||||||
|
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
|
||||||
|
$rng.GetBytes($bytes)
|
||||||
|
$rng.Dispose()
|
||||||
|
return [System.Convert]::ToBase64String($bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate secrets
|
||||||
|
Write-Host "Generating cryptographically secure secrets..." -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$jwtSecret = Generate-Base64String -Length 64
|
||||||
|
$appSecret = Generate-HexString -Length 32
|
||||||
|
$appSecretCode = Generate-HexString -Length 32
|
||||||
|
$encryptionKey = Generate-HexString -Length 16
|
||||||
|
|
||||||
|
# Display generated secrets
|
||||||
|
Write-Host "Generated Secrets:" -ForegroundColor Cyan
|
||||||
|
Write-Host "==================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "JWT_SECRET=" -NoNewline -ForegroundColor White
|
||||||
|
Write-Host $jwtSecret -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "APP_SECRET=" -NoNewline -ForegroundColor White
|
||||||
|
Write-Host $appSecret -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "APP_SECRET_CODE=" -NoNewline -ForegroundColor White
|
||||||
|
Write-Host $appSecretCode -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "ENCRYPTION_KEY=" -NoNewline -ForegroundColor White
|
||||||
|
Write-Host $encryptionKey -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
$envFile = ".env"
|
||||||
|
$envExists = Test-Path $envFile
|
||||||
|
|
||||||
|
if ($envExists) {
|
||||||
|
Write-Host "Warning: .env file already exists!" -ForegroundColor Red
|
||||||
|
$overwrite = Read-Host "Do you want to update it with new secrets? (y/N)"
|
||||||
|
if ($overwrite -eq "y" -or $overwrite -eq "Y") {
|
||||||
|
$updateFile = $true
|
||||||
|
} else {
|
||||||
|
$updateFile = $false
|
||||||
|
Write-Host "Secrets generated but not written to file." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$createFile = Read-Host "Create .env file with these secrets? (Y/n)"
|
||||||
|
if ($createFile -eq "" -or $createFile -eq "y" -or $createFile -eq "Y") {
|
||||||
|
$updateFile = $true
|
||||||
|
} else {
|
||||||
|
$updateFile = $false
|
||||||
|
Write-Host "Secrets generated but not written to file." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updateFile) {
|
||||||
|
# Create or update .env file
|
||||||
|
if ($envExists) {
|
||||||
|
# Backup existing file
|
||||||
|
$backupFile = ".env.backup." + (Get-Date -Format "yyyyMMdd-HHmmss")
|
||||||
|
Copy-Item $envFile $backupFile
|
||||||
|
Write-Host "Backed up existing .env to $backupFile" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Read existing content and update secrets
|
||||||
|
$content = Get-Content $envFile
|
||||||
|
$newContent = @()
|
||||||
|
|
||||||
|
foreach ($line in $content) {
|
||||||
|
if ($line -match "^JWT_SECRET=") {
|
||||||
|
$newContent += "JWT_SECRET=$jwtSecret"
|
||||||
|
} elseif ($line -match "^APP_SECRET=") {
|
||||||
|
$newContent += "APP_SECRET=$appSecret"
|
||||||
|
} elseif ($line -match "^APP_SECRET_CODE=") {
|
||||||
|
$newContent += "APP_SECRET_CODE=$appSecretCode"
|
||||||
|
} elseif ($line -match "^ENCRYPTION_KEY=") {
|
||||||
|
$newContent += "ENCRYPTION_KEY=$encryptionKey"
|
||||||
|
} else {
|
||||||
|
$newContent += $line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$newContent | Out-File -FilePath $envFile -Encoding UTF8
|
||||||
|
Write-Host "Updated .env file with new secrets" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
# Create new .env file from template
|
||||||
|
if (Test-Path ".env.example") {
|
||||||
|
$template = Get-Content ".env.example"
|
||||||
|
$newContent = @()
|
||||||
|
|
||||||
|
foreach ($line in $template) {
|
||||||
|
if ($line -match "^JWT_SECRET=") {
|
||||||
|
$newContent += "JWT_SECRET=$jwtSecret"
|
||||||
|
} elseif ($line -match "^APP_SECRET=") {
|
||||||
|
$newContent += "APP_SECRET=$appSecret"
|
||||||
|
} elseif ($line -match "^APP_SECRET_CODE=") {
|
||||||
|
$newContent += "APP_SECRET_CODE=$appSecretCode"
|
||||||
|
} elseif ($line -match "^ENCRYPTION_KEY=") {
|
||||||
|
$newContent += "ENCRYPTION_KEY=$encryptionKey"
|
||||||
|
} else {
|
||||||
|
$newContent += $line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$newContent | Out-File -FilePath $envFile -Encoding UTF8
|
||||||
|
Write-Host "Created .env file from template with generated secrets" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
# Create minimal .env file
|
||||||
|
$minimalEnv = @(
|
||||||
|
"# ACC Server Manager Environment Configuration",
|
||||||
|
"# Generated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')",
|
||||||
|
"",
|
||||||
|
"# CRITICAL SECURITY SETTINGS (REQUIRED)",
|
||||||
|
"JWT_SECRET=$jwtSecret",
|
||||||
|
"APP_SECRET=$appSecret",
|
||||||
|
"APP_SECRET_CODE=$appSecretCode",
|
||||||
|
"ENCRYPTION_KEY=$encryptionKey",
|
||||||
|
"",
|
||||||
|
"# CORE APPLICATION SETTINGS",
|
||||||
|
"DB_NAME=acc.db",
|
||||||
|
"PORT=3000",
|
||||||
|
"CORS_ALLOWED_ORIGIN=http://localhost:5173",
|
||||||
|
"PASSWORD=change-this-default-admin-password"
|
||||||
|
)
|
||||||
|
|
||||||
|
$minimalEnv | Out-File -FilePath $envFile -Encoding UTF8
|
||||||
|
Write-Host "Created minimal .env file with generated secrets" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Security Notes:" -ForegroundColor Red
|
||||||
|
Write-Host "===============" -ForegroundColor Red
|
||||||
|
Write-Host "1. Keep these secrets secure and never commit them to version control" -ForegroundColor Yellow
|
||||||
|
Write-Host "2. Use different secrets for each environment (dev, staging, production)" -ForegroundColor Yellow
|
||||||
|
Write-Host "3. Rotate secrets regularly in production environments" -ForegroundColor Yellow
|
||||||
|
Write-Host "4. The ENCRYPTION_KEY is exactly 32 bytes as required for AES-256" -ForegroundColor Yellow
|
||||||
|
Write-Host "5. Change the default PASSWORD immediately after first login" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Verify encryption key length
|
||||||
|
if ($encryptionKey.Length -eq 32) { # 32 characters = 32 bytes when converted to []byte in Go
|
||||||
|
Write-Host "✓ Encryption key length verified (32 characters = 32 bytes for AES-256)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "✗ Warning: Encryption key length is incorrect! Got $($encryptionKey.Length) chars, expected 32" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Next steps:" -ForegroundColor Cyan
|
||||||
|
Write-Host "1. Review and customize the .env file if needed" -ForegroundColor White
|
||||||
|
Write-Host "2. Ensure SteamCMD and NSSM are installed and paths are correct" -ForegroundColor White
|
||||||
|
Write-Host "3. Build and run the application: go run cmd/api/main.go" -ForegroundColor White
|
||||||
|
Write-Host "4. Change the default admin password on first login" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Happy racing! 🏁" -ForegroundColor Green
|
||||||
145
scripts/generate-secrets.sh
Normal file
145
scripts/generate-secrets.sh
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ACC Server Manager - Secret Generation Script (Unix/Linux)
|
||||||
|
# This script generates cryptographically secure secrets for the ACC Server Manager
|
||||||
|
|
||||||
|
echo -e "\033[32mACC Server Manager - Secret Generation Script\033[0m"
|
||||||
|
echo -e "\033[32m=============================================\033[0m"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if openssl is available
|
||||||
|
if ! command -v openssl &> /dev/null; then
|
||||||
|
echo -e "\033[31mError: openssl is required but not installed.\033[0m"
|
||||||
|
echo "Please install openssl and try again."
|
||||||
|
echo ""
|
||||||
|
echo "Ubuntu/Debian: sudo apt-get install openssl"
|
||||||
|
echo "CentOS/RHEL: sudo yum install openssl"
|
||||||
|
echo "macOS: brew install openssl"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate secrets using openssl
|
||||||
|
echo -e "\033[33mGenerating cryptographically secure secrets...\033[0m"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
JWT_SECRET=$(openssl rand -base64 64)
|
||||||
|
APP_SECRET=$(openssl rand -hex 32)
|
||||||
|
APP_SECRET_CODE=$(openssl rand -hex 32)
|
||||||
|
ENCRYPTION_KEY=$(openssl rand -hex 16)
|
||||||
|
|
||||||
|
# Display generated secrets
|
||||||
|
echo -e "\033[36mGenerated Secrets:\033[0m"
|
||||||
|
echo -e "\033[36m==================\033[0m"
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[37mJWT_SECRET=\033[0m\033[33m$JWT_SECRET\033[0m"
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[37mAPP_SECRET=\033[0m\033[33m$APP_SECRET\033[0m"
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[37mAPP_SECRET_CODE=\033[0m\033[33m$APP_SECRET_CODE\033[0m"
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[37mENCRYPTION_KEY=\033[0m\033[33m$ENCRYPTION_KEY\033[0m"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
ENV_FILE=".env"
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
echo -e "\033[31mWarning: .env file already exists!\033[0m"
|
||||||
|
read -p "Do you want to update it with new secrets? (y/N): " overwrite
|
||||||
|
if [[ $overwrite =~ ^[Yy]$ ]]; then
|
||||||
|
UPDATE_FILE=true
|
||||||
|
else
|
||||||
|
UPDATE_FILE=false
|
||||||
|
echo -e "\033[33mSecrets generated but not written to file.\033[0m"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
read -p "Create .env file with these secrets? (Y/n): " create_file
|
||||||
|
if [[ $create_file =~ ^[Nn]$ ]]; then
|
||||||
|
UPDATE_FILE=false
|
||||||
|
echo -e "\033[33mSecrets generated but not written to file.\033[0m"
|
||||||
|
else
|
||||||
|
UPDATE_FILE=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$UPDATE_FILE" = true ]; then
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
# Backup existing file
|
||||||
|
BACKUP_FILE=".env.backup.$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp "$ENV_FILE" "$BACKUP_FILE"
|
||||||
|
echo -e "\033[32mBacked up existing .env to $BACKUP_FILE\033[0m"
|
||||||
|
|
||||||
|
# Update existing file
|
||||||
|
if command -v sed &> /dev/null; then
|
||||||
|
# Use sed to update secrets in place
|
||||||
|
sed -i.tmp "s/^JWT_SECRET=.*/JWT_SECRET=$JWT_SECRET/" "$ENV_FILE"
|
||||||
|
sed -i.tmp "s/^APP_SECRET=.*/APP_SECRET=$APP_SECRET/" "$ENV_FILE"
|
||||||
|
sed -i.tmp "s/^APP_SECRET_CODE=.*/APP_SECRET_CODE=$APP_SECRET_CODE/" "$ENV_FILE"
|
||||||
|
sed -i.tmp "s/^ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$ENCRYPTION_KEY/" "$ENV_FILE"
|
||||||
|
rm -f "$ENV_FILE.tmp"
|
||||||
|
echo -e "\033[32mUpdated .env file with new secrets\033[0m"
|
||||||
|
else
|
||||||
|
echo -e "\033[31mError: sed command not found. Please update the .env file manually.\033[0m"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new .env file
|
||||||
|
if [ -f ".env.example" ]; then
|
||||||
|
# Create from template
|
||||||
|
cp ".env.example" "$ENV_FILE"
|
||||||
|
if command -v sed &> /dev/null; then
|
||||||
|
sed -i.tmp "s/^JWT_SECRET=.*/JWT_SECRET=$JWT_SECRET/" "$ENV_FILE"
|
||||||
|
sed -i.tmp "s/^APP_SECRET=.*/APP_SECRET=$APP_SECRET/" "$ENV_FILE"
|
||||||
|
sed -i.tmp "s/^APP_SECRET_CODE=.*/APP_SECRET_CODE=$APP_SECRET_CODE/" "$ENV_FILE"
|
||||||
|
sed -i.tmp "s/^ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$ENCRYPTION_KEY/" "$ENV_FILE"
|
||||||
|
rm -f "$ENV_FILE.tmp"
|
||||||
|
echo -e "\033[32mCreated .env file from template with generated secrets\033[0m"
|
||||||
|
else
|
||||||
|
echo -e "\033[31mError: sed command not found. Please update the .env file manually.\033[0m"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create minimal .env file
|
||||||
|
cat > "$ENV_FILE" << EOF
|
||||||
|
# ACC Server Manager Environment Configuration
|
||||||
|
# Generated on $(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# CRITICAL SECURITY SETTINGS (REQUIRED)
|
||||||
|
JWT_SECRET=$JWT_SECRET
|
||||||
|
APP_SECRET=$APP_SECRET
|
||||||
|
APP_SECRET_CODE=$APP_SECRET_CODE
|
||||||
|
ENCRYPTION_KEY=$ENCRYPTION_KEY
|
||||||
|
|
||||||
|
# CORE APPLICATION SETTINGS
|
||||||
|
DB_NAME=acc.db
|
||||||
|
PORT=3000
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:5173
|
||||||
|
PASSWORD=change-this-default-admin-password
|
||||||
|
EOF
|
||||||
|
echo -e "\033[32mCreated minimal .env file with generated secrets\033[0m"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[31mSecurity Notes:\033[0m"
|
||||||
|
echo -e "\033[31m===============\033[0m"
|
||||||
|
echo -e "\033[33m1. Keep these secrets secure and never commit them to version control\033[0m"
|
||||||
|
echo -e "\033[33m2. Use different secrets for each environment (dev, staging, production)\033[0m"
|
||||||
|
echo -e "\033[33m3. Rotate secrets regularly in production environments\033[0m"
|
||||||
|
echo -e "\033[33m4. The ENCRYPTION_KEY is exactly 32 bytes as required for AES-256\033[0m"
|
||||||
|
echo -e "\033[33m5. Change the default PASSWORD immediately after first login\033[0m"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify encryption key length (32 characters = 32 bytes when converted to []byte in Go)
|
||||||
|
if [ ${#ENCRYPTION_KEY} -eq 32 ]; then
|
||||||
|
echo -e "\033[32m✓ Encryption key length verified (32 characters = 32 bytes for AES-256)\033[0m"
|
||||||
|
else
|
||||||
|
echo -e "\033[31m✗ Warning: Encryption key length is incorrect!\033[0m"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[36mNext steps:\033[0m"
|
||||||
|
echo -e "\033[37m1. Review and customize the .env file if needed\033[0m"
|
||||||
|
echo -e "\033[37m2. Install Go 1.23.0 or later if not already installed\033[0m"
|
||||||
|
echo -e "\033[37m3. Build and run the application: go run cmd/api/main.go\033[0m"
|
||||||
|
echo -e "\033[37m4. Change the default admin password on first login\033[0m"
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[32mHappy racing! 🏁\033[0m"
|
||||||
Reference in New Issue
Block a user