init bootstrap
This commit is contained in:
226
.env.example
Normal file
226
.env.example
Normal file
@@ -0,0 +1,226 @@
|
||||
# Bootstrap App Environment Configuration
|
||||
# Copy this file to .env and update the values
|
||||
|
||||
# =============================================================================
|
||||
# CRITICAL SECURITY SETTINGS (REQUIRED)
|
||||
# =============================================================================
|
||||
# These values MUST be set for the application to work
|
||||
# Use the scripts/generate-secrets.* scripts to generate secure values
|
||||
|
||||
# JWT Secret for token signing (64+ characters, base64 encoded)
|
||||
JWT_SECRET=your-generated-jwt-secret-here
|
||||
|
||||
# Application secrets (32 bytes, hex encoded)
|
||||
APP_SECRET=your-generated-app-secret-here
|
||||
APP_SECRET_CODE=your-generated-secret-code-here
|
||||
|
||||
# AES-256 encryption key (exactly 32 characters, hex encoded)
|
||||
ENCRYPTION_KEY=your-generated-32-character-hex-key
|
||||
|
||||
# =============================================================================
|
||||
# CORE APPLICATION SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# Server port
|
||||
PORT=3000
|
||||
|
||||
# Database configuration
|
||||
DB_NAME=app.db
|
||||
|
||||
# CORS configuration (comma-separated for multiple origins)
|
||||
CORS_ALLOWED_ORIGIN=http://localhost:5173,http://localhost:3000
|
||||
|
||||
# =============================================================================
|
||||
# AUTHENTICATION & SECURITY
|
||||
# =============================================================================
|
||||
|
||||
# JWT token configuration
|
||||
JWT_ACCESS_TTL_HOURS=24
|
||||
JWT_REFRESH_TTL_DAYS=7
|
||||
JWT_ISSUER=omega-server
|
||||
|
||||
# Password policy
|
||||
PASSWORD_MIN_LENGTH=8
|
||||
MAX_LOGIN_ATTEMPTS=5
|
||||
LOCKOUT_DURATION_MINUTES=30
|
||||
|
||||
# Rate limiting
|
||||
RATE_LIMIT_REQUESTS=100
|
||||
RATE_LIMIT_WINDOW_MINUTES=1
|
||||
|
||||
# Session timeout
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
|
||||
# =============================================================================
|
||||
# DEFAULT ADMIN ACCOUNT
|
||||
# =============================================================================
|
||||
|
||||
# Default admin password (CHANGE THIS IMMEDIATELY AFTER FIRST LOGIN)
|
||||
DEFAULT_ADMIN_PASSWORD=change-this-password
|
||||
|
||||
# =============================================================================
|
||||
# LOGGING CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Log level: DEBUG, INFO, WARN, ERROR, PANIC
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Enable debug mode (shows detailed error messages)
|
||||
DEBUG_MODE=false
|
||||
|
||||
# Log retention in days
|
||||
LOG_RETENTION_DAYS=30
|
||||
|
||||
# =============================================================================
|
||||
# ENVIRONMENT SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# Environment: development, staging, production
|
||||
GO_ENV=development
|
||||
|
||||
# =============================================================================
|
||||
# EMAIL CONFIGURATION (Optional)
|
||||
# =============================================================================
|
||||
|
||||
# SMTP settings for email notifications
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_EMAIL=noreply@example.com
|
||||
SMTP_FROM_NAME=Bootstrap App
|
||||
|
||||
# Enable TLS for SMTP
|
||||
SMTP_USE_TLS=true
|
||||
|
||||
# =============================================================================
|
||||
# FILE UPLOAD SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# Maximum file upload size in MB
|
||||
MAX_FILE_UPLOAD_SIZE_MB=10
|
||||
|
||||
# Allowed file extensions (comma-separated)
|
||||
ALLOWED_FILE_EXTENSIONS=jpg,jpeg,png,gif,pdf,doc,docx,txt
|
||||
|
||||
# Upload directory
|
||||
UPLOAD_DIR=uploads
|
||||
|
||||
# =============================================================================
|
||||
# CACHE CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Enable caching
|
||||
CACHE_ENABLED=true
|
||||
|
||||
# Cache TTL in minutes
|
||||
CACHE_TTL_MINUTES=60
|
||||
|
||||
# =============================================================================
|
||||
# API CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# API rate limiting per endpoint
|
||||
API_RATE_LIMIT_REQUESTS=1000
|
||||
API_RATE_LIMIT_WINDOW_MINUTES=60
|
||||
|
||||
# API request timeout in seconds
|
||||
API_REQUEST_TIMEOUT_SECONDS=30
|
||||
|
||||
# =============================================================================
|
||||
# MONITORING & HEALTH CHECKS
|
||||
# =============================================================================
|
||||
|
||||
# Enable health check endpoint
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
|
||||
# Health check interval in seconds
|
||||
HEALTH_CHECK_INTERVAL_SECONDS=30
|
||||
|
||||
# =============================================================================
|
||||
# DEVELOPMENT SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# Enable request logging in development
|
||||
DEV_LOG_REQUESTS=true
|
||||
|
||||
# Enable SQL query logging
|
||||
DEV_LOG_SQL_QUERIES=false
|
||||
|
||||
# Enable detailed error responses
|
||||
DEV_DETAILED_ERRORS=true
|
||||
|
||||
# =============================================================================
|
||||
# SECURITY HEADERS
|
||||
# =============================================================================
|
||||
|
||||
# Content Security Policy
|
||||
CSP_POLICY=default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'
|
||||
|
||||
# X-Frame-Options
|
||||
X_FRAME_OPTIONS=DENY
|
||||
|
||||
# X-Content-Type-Options
|
||||
X_CONTENT_TYPE_OPTIONS=nosniff
|
||||
|
||||
# Referrer Policy
|
||||
REFERRER_POLICY=strict-origin-when-cross-origin
|
||||
|
||||
# =============================================================================
|
||||
# BACKUP SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# Enable automatic database backups
|
||||
BACKUP_ENABLED=true
|
||||
|
||||
# Backup interval in hours
|
||||
BACKUP_INTERVAL_HOURS=24
|
||||
|
||||
# Backup retention in days
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
|
||||
# Backup directory
|
||||
BACKUP_DIR=backups
|
||||
|
||||
# =============================================================================
|
||||
# FEATURE FLAGS
|
||||
# =============================================================================
|
||||
|
||||
# Enable user registration
|
||||
ENABLE_USER_REGISTRATION=true
|
||||
|
||||
# Enable email verification
|
||||
ENABLE_EMAIL_VERIFICATION=false
|
||||
|
||||
# Enable two-factor authentication
|
||||
ENABLE_TWO_FACTOR_AUTH=false
|
||||
|
||||
# Enable audit logging
|
||||
ENABLE_AUDIT_LOGGING=true
|
||||
|
||||
# Enable security event logging
|
||||
ENABLE_SECURITY_EVENT_LOGGING=true
|
||||
|
||||
# =============================================================================
|
||||
# EXTERNAL SERVICES (Optional)
|
||||
# =============================================================================
|
||||
|
||||
# Redis configuration (if using Redis for caching/sessions)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# External API keys
|
||||
EXTERNAL_API_KEY=
|
||||
EXTERNAL_API_SECRET=
|
||||
|
||||
# =============================================================================
|
||||
# NOTES
|
||||
# =============================================================================
|
||||
|
||||
# 1. Never commit this file with real secrets to version control
|
||||
# 2. Use the scripts/generate-secrets.* scripts to generate secure secrets
|
||||
# 3. Change the DEFAULT_ADMIN_PASSWORD immediately after first setup
|
||||
# 4. Review and adjust security settings based on your deployment environment
|
||||
# 5. Enable HTTPS in production and update CORS_ALLOWED_ORIGIN accordingly
|
||||
172
.gitignore
vendored
Normal file
172
.gitignore
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
.env.staging
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
*.log.*
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Editor backups
|
||||
*~
|
||||
*.bak
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
bin/
|
||||
pkg/
|
||||
|
||||
# Coverage files
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
# Air live reload
|
||||
tmp/
|
||||
|
||||
# Documentation generated files
|
||||
docs/swagger.json
|
||||
docs/swagger.yaml
|
||||
|
||||
# Secrets and certificates
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# Cache directories
|
||||
.cache/
|
||||
node_modules/
|
||||
|
||||
# Temporary files
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*.back
|
||||
|
||||
# Compressed files
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.7z
|
||||
*.rar
|
||||
|
||||
# Local configuration override files
|
||||
config.local.*
|
||||
*.local.json
|
||||
|
||||
# Test fixtures
|
||||
test_data/
|
||||
fixtures/
|
||||
|
||||
# Generated documentation
|
||||
docs/generated/
|
||||
|
||||
# Profiling files
|
||||
*.prof
|
||||
*.pprof
|
||||
|
||||
# Memory dumps
|
||||
*.hprof
|
||||
|
||||
# JetBrains
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# Sublime Text
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
*.un~
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Zed editor
|
||||
.zed/
|
||||
|
||||
# Application specific
|
||||
app.db
|
||||
omega-server.exe
|
||||
api.exe
|
||||
465
README.md
Normal file
465
README.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# Bootstrap App
|
||||
|
||||
A comprehensive web-based application built with clean architecture principles using Go and Fiber framework. This project serves as a robust foundation for building scalable web applications with advanced security features, role-based access control, and comprehensive API management.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
### Core Application Features
|
||||
- **Clean Architecture**: Layered architecture with clear separation of concerns
|
||||
- **RESTful API**: Comprehensive REST API with Swagger documentation
|
||||
- **Database Management**: SQLite with GORM ORM and automatic migrations
|
||||
- **Dependency Injection**: Uber Dig container for clean dependency management
|
||||
|
||||
### Security Features
|
||||
- **JWT Authentication**: Secure token-based authentication system
|
||||
- **Role-Based Access Control**: 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
|
||||
- **Audit Logging**: Comprehensive logging and audit trails
|
||||
|
||||
### Monitoring & Management
|
||||
- **Health Checks**: Built-in health check endpoints
|
||||
- **Request Logging**: Detailed HTTP request and response logging
|
||||
- **Security Event Tracking**: Real-time security event monitoring
|
||||
- **System Configuration**: Dynamic configuration management
|
||||
- **Cache Management**: In-memory caching with TTL support
|
||||
|
||||
## 🏗️ 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
|
||||
- **Caching**: In-memory cache with automatic cleanup
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
omega-server/
|
||||
├── cmd/
|
||||
│ └── api/ # Application entry point
|
||||
├── local/
|
||||
│ ├── api/ # API route definitions
|
||||
│ ├── controller/ # HTTP request handlers
|
||||
│ ├── middleware/ # Authentication and security middleware
|
||||
│ │ └── security/ # Advanced security middleware
|
||||
│ ├── model/ # Database models and business entities
|
||||
│ ├── repository/ # Data access layer
|
||||
│ ├── service/ # Business logic services
|
||||
│ └── utl/ # Utilities and shared components
|
||||
│ ├── cache/ # Caching utilities
|
||||
│ ├── common/ # Common utilities and types
|
||||
│ ├── configs/ # Configuration management
|
||||
│ ├── db/ # Database connection and migration
|
||||
│ ├── jwt/ # JWT token management
|
||||
│ ├── logging/ # Logging utilities
|
||||
│ ├── password/ # Password hashing utilities
|
||||
│ └── server/ # HTTP server configuration
|
||||
├── docs/ # Documentation
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── generate-secrets.ps1 # PowerShell secret generation
|
||||
│ └── generate-secrets.sh # Bash secret generation
|
||||
├── logs/ # Application logs
|
||||
├── go.mod # Go module definition
|
||||
├── .env.example # Environment template
|
||||
└── .gitignore # Git ignore rules
|
||||
```
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- **Go**: Version 1.23.0 or later
|
||||
- **Operating System**: Windows, Linux, or macOS
|
||||
- **Memory**: Minimum 512MB RAM
|
||||
- **Disk Space**: 100MB for application and logs
|
||||
|
||||
### Development Tools (Optional)
|
||||
- **Air**: For hot reloading during development
|
||||
- **Git**: For version control
|
||||
- **Make**: For build automation
|
||||
- **Docker**: For containerized deployment
|
||||
|
||||
## ⚙️ Installation
|
||||
|
||||
### 1. Clone or Copy the Project
|
||||
```bash
|
||||
# If using git
|
||||
git clone <your-repository-url>
|
||||
cd omega-server
|
||||
|
||||
# Or copy the bootstrap folder to your desired location
|
||||
```
|
||||
|
||||
### 2. Generate Secure Secrets
|
||||
We provide scripts to automatically generate secure secrets and create your `.env` file:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\scripts\generate-secrets.ps1
|
||||
```
|
||||
|
||||
**Linux/macOS (Bash):**
|
||||
```bash
|
||||
chmod +x scripts/generate-secrets.sh
|
||||
./scripts/generate-secrets.sh
|
||||
```
|
||||
|
||||
**Manual Setup:**
|
||||
If you prefer to set up manually:
|
||||
```bash
|
||||
cp .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
|
||||
```
|
||||
|
||||
### 3. Install Dependencies
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
### 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 environment variables for configuration. Most critical settings are automatically generated by the secret generation scripts:
|
||||
|
||||
| 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 | app.db | SQLite database filename |
|
||||
| `CORS_ALLOWED_ORIGIN` | No | http://localhost:5173 | CORS allowed origin |
|
||||
| `DEFAULT_ADMIN_PASSWORD` | No | - | Default admin password for initial setup |
|
||||
| `LOG_LEVEL` | No | INFO | Logging level (DEBUG, INFO, WARN, ERROR) |
|
||||
|
||||
### System Configuration
|
||||
|
||||
Advanced settings can be managed through the web interface after initial setup:
|
||||
- **Security Policies**: Rate limits, session timeouts, password policies
|
||||
- **Monitoring**: Logging levels, audit settings
|
||||
- **Cache Settings**: TTL configuration, cache policies
|
||||
- **API Configuration**: Rate limiting, request timeouts
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
This application implements comprehensive security measures:
|
||||
|
||||
### Authentication & Authorization
|
||||
- **JWT Tokens**: Secure token-based authentication with configurable TTL
|
||||
- **Password Security**: Bcrypt hashing with strength validation
|
||||
- **Role-Based Access**: Granular permission system
|
||||
- **Session Management**: Configurable timeouts and automatic cleanup
|
||||
|
||||
### Protection Mechanisms
|
||||
- **Rate Limiting**: Multiple layers of rate limiting (global and per-endpoint)
|
||||
- **Input Validation**: Comprehensive input sanitization
|
||||
- **Security Headers**: OWASP-compliant HTTP headers
|
||||
- **CORS Protection**: Configurable cross-origin restrictions
|
||||
- **Request Limits**: Size and timeout limitations
|
||||
|
||||
### Monitoring & Auditing
|
||||
- **Security Events**: Authentication and authorization logging
|
||||
- **Audit Trail**: Comprehensive activity logging with user tracking
|
||||
- **Threat Detection**: Suspicious activity monitoring
|
||||
- **Real-time Alerts**: Immediate notification of security events
|
||||
|
||||
## 📚 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
|
||||
|
||||
#### Health & Status
|
||||
- `GET /health` - Health check endpoint
|
||||
- `GET /ping` - Simple ping endpoint
|
||||
|
||||
#### Authentication
|
||||
- `POST /api/v1/auth/login` - User authentication
|
||||
- `POST /api/v1/auth/register` - User registration
|
||||
- `POST /api/v1/auth/refresh` - Refresh access token
|
||||
- `POST /api/v1/auth/logout` - User logout
|
||||
|
||||
#### User Management
|
||||
- `GET /api/v1/users` - List all users (paginated)
|
||||
- `POST /api/v1/users` - Create new user
|
||||
- `GET /api/v1/users/{id}` - Get user details
|
||||
- `PUT /api/v1/users/{id}` - Update user
|
||||
- `DELETE /api/v1/users/{id}` - Delete user
|
||||
|
||||
#### Role & Permission Management
|
||||
- `GET /api/v1/roles` - List all roles
|
||||
- `POST /api/v1/roles` - Create new role
|
||||
- `GET /api/v1/permissions` - List all permissions
|
||||
- `POST /api/v1/roles/{id}/permissions` - Assign permissions to role
|
||||
|
||||
#### System Management
|
||||
- `GET /api/v1/system/config` - Get system configuration
|
||||
- `PUT /api/v1/system/config` - Update system configuration
|
||||
- `GET /api/v1/system/audit-logs` - Get audit logs
|
||||
- `GET /api/v1/system/security-events` - Get security events
|
||||
|
||||
## 🛠️ 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
|
||||
```
|
||||
|
||||
### Environment Setup for Development
|
||||
1. Copy `.env.example` to `.env`
|
||||
2. Run the secret generation script
|
||||
3. Set `LOG_LEVEL=DEBUG` for detailed logging
|
||||
4. Set `DEBUG_MODE=true` for detailed error responses
|
||||
|
||||
### Database Management
|
||||
```bash
|
||||
# View database schema
|
||||
sqlite3 app.db ".schema"
|
||||
|
||||
# View data
|
||||
sqlite3 app.db "SELECT * FROM users;"
|
||||
|
||||
# Backup database
|
||||
cp app.db app_backup.db
|
||||
```
|
||||
|
||||
### Adding New Features
|
||||
|
||||
#### 1. Create a New Model
|
||||
```go
|
||||
// In local/model/your_model.go
|
||||
type YourModel struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
// Add your fields
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Create Repository
|
||||
```go
|
||||
// In local/repository/your_repository.go
|
||||
type YourRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *YourRepository) Create(model *YourModel) error {
|
||||
return r.db.Create(model).Error
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Create Service
|
||||
```go
|
||||
// In local/service/your_service.go
|
||||
type YourService struct {
|
||||
repo *repository.YourRepository
|
||||
}
|
||||
|
||||
func (s *YourService) CreateItem(req CreateRequest) (*YourModel, error) {
|
||||
// Business logic here
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Create Controller
|
||||
```go
|
||||
// In local/controller/your_controller.go
|
||||
type YourController struct {
|
||||
service *service.YourService
|
||||
}
|
||||
|
||||
func (c *YourController) Create(ctx *fiber.Ctx) error {
|
||||
// Handle HTTP request
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Production Deployment
|
||||
|
||||
### 1. Generate Production Secrets
|
||||
```bash
|
||||
# Use the secret generation script for production
|
||||
./scripts/generate-secrets.sh # Linux/macOS
|
||||
.\scripts\generate-secrets.ps1 # Windows
|
||||
```
|
||||
|
||||
### 2. Build for Production
|
||||
```bash
|
||||
# Build optimized binary
|
||||
go build -ldflags="-w -s" -o omega-server.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 monitoring and alerting
|
||||
- [ ] Set up log rotation
|
||||
- [ ] Test all security configurations
|
||||
|
||||
### 4. Environment Variables for Production
|
||||
```bash
|
||||
# Set these in your production environment
|
||||
GO_ENV=production
|
||||
LOG_LEVEL=WARN
|
||||
CORS_ALLOWED_ORIGIN=https://yourdomain.com
|
||||
DEFAULT_ADMIN_PASSWORD=your-secure-password
|
||||
```
|
||||
|
||||
### 5. Monitoring Setup
|
||||
- Configure log rotation and retention
|
||||
- Set up health check monitoring
|
||||
- Configure alerting for critical errors
|
||||
- Monitor resource usage and performance
|
||||
- Set up database backup procedures
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "JWT_SECRET environment variable is required"
|
||||
**Solution**: Run the secret generation script or manually set the JWT_SECRET environment variable.
|
||||
|
||||
#### "Failed to connect database"
|
||||
**Solution**: Ensure the application has write permissions to the database directory and the database file doesn't exist or is accessible.
|
||||
|
||||
#### Database Migration Errors
|
||||
**Solution**: Delete the database file and restart the application to recreate it with the latest schema:
|
||||
```bash
|
||||
rm app.db
|
||||
./api.exe
|
||||
```
|
||||
|
||||
#### Permission Denied Errors
|
||||
**Solution**: Ensure the application has proper file system permissions:
|
||||
```bash
|
||||
# Linux/macOS
|
||||
chmod +x api.exe
|
||||
chmod 755 logs/
|
||||
|
||||
# Windows - Run as Administrator if needed
|
||||
```
|
||||
|
||||
### Log Locations
|
||||
- **Application Logs**: `./logs/app_YYYY-MM-DD.log`
|
||||
- **Database File**: `./app.db` (or as configured in DB_NAME)
|
||||
- **Environment Config**: `./.env`
|
||||
|
||||
### Debug Mode
|
||||
Enable debug logging and detailed error messages:
|
||||
```bash
|
||||
# In .env file
|
||||
LOG_LEVEL=DEBUG
|
||||
DEBUG_MODE=true
|
||||
```
|
||||
|
||||
## 📈 Performance Considerations
|
||||
|
||||
### Database Optimization
|
||||
- Indexes are automatically created for frequently queried fields
|
||||
- Use pagination for large result sets
|
||||
- Consider database cleanup for old audit logs and security events
|
||||
|
||||
### Caching
|
||||
- Built-in memory cache with configurable TTL
|
||||
- Cache frequently accessed configuration data
|
||||
- Automatic cleanup of expired cache entries
|
||||
|
||||
### Rate Limiting
|
||||
- Adjust rate limits based on your application's needs
|
||||
- Monitor rate limit metrics in production
|
||||
- Consider implementing distributed rate limiting for multiple instances
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Development Workflow
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||
3. Make your changes following the established patterns
|
||||
4. Add tests for new functionality
|
||||
5. Ensure all tests pass: `go test ./...`
|
||||
6. Update documentation if needed
|
||||
7. Commit your changes: `git commit -m 'Add amazing feature'`
|
||||
8. Push to the branch: `git push origin feature/amazing-feature`
|
||||
9. Open a Pull Request
|
||||
|
||||
### Code Style Guidelines
|
||||
- Follow Go best practices and conventions
|
||||
- Use `gofmt` for code formatting
|
||||
- Add comprehensive comments for public functions
|
||||
- Follow the established project structure
|
||||
- Include error handling for all operations
|
||||
|
||||
### Adding New Dependencies
|
||||
- Use `go mod tidy` after adding dependencies
|
||||
- Ensure licenses are compatible
|
||||
- Document any new dependencies in the README
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **Fiber Framework**: High-performance HTTP framework for Go
|
||||
- **GORM**: The fantastic ORM library for Go
|
||||
- **Uber Dig**: Dependency injection container
|
||||
- **JWT-Go**: JWT implementation for Go
|
||||
- **Bcrypt**: Secure password hashing
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Documentation
|
||||
- [API Documentation](http://localhost:3000/swagger/)
|
||||
- [Architecture Guide](docs/ARCHITECTURE.md)
|
||||
- [Security Guide](docs/SECURITY.md)
|
||||
- [Deployment Guide](docs/DEPLOYMENT.md)
|
||||
|
||||
### Getting Help
|
||||
- Check the [Troubleshooting](#troubleshooting) section
|
||||
- Review the comprehensive inline code documentation
|
||||
- Check existing issues in the repository
|
||||
- Create a new issue with detailed information
|
||||
|
||||
---
|
||||
|
||||
**Happy Coding! 🚀**
|
||||
|
||||
Built with ❤️ using clean architecture principles and modern security practices.
|
||||
48
cmd/api/main.go
Normal file
48
cmd/api/main.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"omega-server/local/utl/cache"
|
||||
"omega-server/local/utl/db"
|
||||
"omega-server/local/utl/logging"
|
||||
"omega-server/local/utl/server"
|
||||
"os"
|
||||
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// @title Bootstrap App API
|
||||
// @version 1.0
|
||||
// @description A comprehensive web-based management system built with clean architecture principles
|
||||
// @contact.name API Support
|
||||
// @contact.email support@example.com
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
// @host localhost:3000
|
||||
// @BasePath /api/v1
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
func main() {
|
||||
// Initialize logger
|
||||
logger, err := logging.Initialize()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
// Set up panic recovery
|
||||
defer logging.RecoverAndLog()
|
||||
|
||||
logging.Info("Starting Bootstrap App...")
|
||||
|
||||
// Initialize dependency injection container
|
||||
di := dig.New()
|
||||
|
||||
// Initialize core components
|
||||
cache.Start(di)
|
||||
db.Start(di)
|
||||
server.Start(di)
|
||||
}
|
||||
423
docs/ARCHITECTURE.md
Normal file
423
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# Architecture Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the architecture of the Bootstrap App, which follows clean architecture principles with clear separation of concerns, dependency injection, and modern security practices.
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### Clean Architecture
|
||||
The application follows Uncle Bob's Clean Architecture principles:
|
||||
- **Independence of Frameworks**: The architecture doesn't depend on specific frameworks
|
||||
- **Testability**: Business rules can be tested without UI, database, or external services
|
||||
- **Independence of UI**: The UI can change without changing the business rules
|
||||
- **Independence of Database**: Business rules are not bound to the database
|
||||
- **Independence of External Services**: Business rules know nothing about the outside world
|
||||
|
||||
### SOLID Principles
|
||||
- **Single Responsibility**: Each class/module has one reason to change
|
||||
- **Open/Closed**: Open for extension, closed for modification
|
||||
- **Liskov Substitution**: Objects should be replaceable with instances of their subtypes
|
||||
- **Interface Segregation**: Many client-specific interfaces are better than one general-purpose interface
|
||||
- **Dependency Inversion**: Depend on abstractions, not concretions
|
||||
|
||||
## Layer Architecture
|
||||
|
||||
### 1. Presentation Layer (`controller/`)
|
||||
**Responsibility**: Handle HTTP requests and responses
|
||||
- Input validation and sanitization
|
||||
- Request/response mapping
|
||||
- Error handling and status codes
|
||||
- Authentication and authorization checks
|
||||
|
||||
**Components**:
|
||||
- Controllers for each domain
|
||||
- Middleware for cross-cutting concerns
|
||||
- Request/response DTOs
|
||||
|
||||
**Dependencies**: Service Layer
|
||||
|
||||
### 2. Business Logic Layer (`service/`)
|
||||
**Responsibility**: Implement business rules and orchestrate operations
|
||||
- Business rule validation
|
||||
- Transaction management
|
||||
- Cross-domain operations
|
||||
- Business logic orchestration
|
||||
|
||||
**Components**:
|
||||
- Service classes for each domain
|
||||
- Business rule validators
|
||||
- Transaction coordinators
|
||||
|
||||
**Dependencies**: Repository Layer, External Services
|
||||
|
||||
### 3. Data Access Layer (`repository/`)
|
||||
**Responsibility**: Data persistence and retrieval
|
||||
- Database operations
|
||||
- Query optimization
|
||||
- Data mapping
|
||||
- Cache management
|
||||
|
||||
**Components**:
|
||||
- Repository interfaces and implementations
|
||||
- Database models
|
||||
- Query builders
|
||||
|
||||
**Dependencies**: Database, Cache
|
||||
|
||||
### 4. Domain Layer (`model/`)
|
||||
**Responsibility**: Core business entities and rules
|
||||
- Domain entities
|
||||
- Value objects
|
||||
- Domain services
|
||||
- Business rule definitions
|
||||
|
||||
**Components**:
|
||||
- Entity models
|
||||
- Validation rules
|
||||
- Domain constants
|
||||
- Business exceptions
|
||||
|
||||
**Dependencies**: None (should be isolated)
|
||||
|
||||
### 5. Infrastructure Layer (`utl/`)
|
||||
**Responsibility**: External concerns and utilities
|
||||
- Database configuration
|
||||
- Logging
|
||||
- Security utilities
|
||||
- Configuration management
|
||||
- External service integrations
|
||||
|
||||
**Components**:
|
||||
- Database connection and migration
|
||||
- Logging infrastructure
|
||||
- Security middleware
|
||||
- Configuration loaders
|
||||
- Utility functions
|
||||
|
||||
## Component Interaction Flow
|
||||
|
||||
```
|
||||
HTTP Request
|
||||
↓
|
||||
[Middleware Stack] ← Security, Logging, CORS
|
||||
↓
|
||||
[Controller] ← Request handling, validation
|
||||
↓
|
||||
[Service] ← Business logic, orchestration
|
||||
↓
|
||||
[Repository] ← Data access, persistence
|
||||
↓
|
||||
[Database/Cache] ← Data storage
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
The application uses Uber Dig for dependency injection, providing:
|
||||
- **Loose Coupling**: Components depend on interfaces, not implementations
|
||||
- **Testability**: Easy to mock dependencies for testing
|
||||
- **Configuration**: Centralized dependency configuration
|
||||
- **Lifecycle Management**: Automatic instance management
|
||||
|
||||
### DI Container Structure
|
||||
```go
|
||||
// Database and core services
|
||||
di.Provide(func() *gorm.DB { ... })
|
||||
di.Provide(NewCache)
|
||||
|
||||
// Repositories
|
||||
di.Provide(NewUserRepository)
|
||||
di.Provide(NewRoleRepository)
|
||||
// ...
|
||||
|
||||
// Services
|
||||
di.Provide(NewUserService)
|
||||
di.Provide(NewAuthService)
|
||||
// ...
|
||||
|
||||
// Controllers
|
||||
di.Invoke(NewUserController)
|
||||
di.Invoke(NewAuthController)
|
||||
// ...
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Authentication Flow
|
||||
```
|
||||
Client Request
|
||||
↓
|
||||
[CORS Middleware] ← Cross-origin validation
|
||||
↓
|
||||
[Security Headers] ← Security headers injection
|
||||
↓
|
||||
[Rate Limiting] ← Request rate validation
|
||||
↓
|
||||
[JWT Validation] ← Token verification
|
||||
↓
|
||||
[Permission Check] ← Authorization validation
|
||||
↓
|
||||
[Controller] ← Authorized request processing
|
||||
```
|
||||
|
||||
### Security Layers
|
||||
1. **Network Security**: CORS, security headers, HTTPS
|
||||
2. **Authentication**: JWT token validation
|
||||
3. **Authorization**: Role-based access control
|
||||
4. **Input Security**: Sanitization, validation, size limits
|
||||
5. **Audit Security**: Logging, monitoring, alerting
|
||||
|
||||
## Database Architecture
|
||||
|
||||
### Entity Relationships
|
||||
```
|
||||
Users
|
||||
├── UserRoles (Many-to-Many with Roles)
|
||||
├── AuditLogs (One-to-Many)
|
||||
└── SecurityEvents (One-to-Many)
|
||||
|
||||
Roles
|
||||
├── UserRoles (Many-to-Many with Users)
|
||||
└── RolePermissions (Many-to-Many with Permissions)
|
||||
|
||||
Permissions
|
||||
└── RolePermissions (Many-to-Many with Roles)
|
||||
|
||||
SystemConfig (Standalone)
|
||||
```
|
||||
|
||||
### Base Model Pattern
|
||||
All entities inherit from `BaseModel`:
|
||||
```go
|
||||
type BaseModel struct {
|
||||
ID string `json:"id" gorm:"primary_key"`
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
DateUpdated time.Time `json:"dateUpdated"`
|
||||
}
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Consistent ID generation (UUID)
|
||||
- Automatic timestamp management
|
||||
- Uniform audit trail
|
||||
- Simplified queries
|
||||
|
||||
## API Architecture
|
||||
|
||||
### RESTful Design
|
||||
- **Resource-based URLs**: `/api/v1/users/{id}`
|
||||
- **HTTP Methods**: GET, POST, PUT, DELETE, PATCH
|
||||
- **Status Codes**: Appropriate HTTP status codes
|
||||
- **Content Negotiation**: JSON request/response format
|
||||
|
||||
### API Versioning
|
||||
- **URL Versioning**: `/api/v1/`
|
||||
- **Backward Compatibility**: Maintain older versions
|
||||
- **Migration Path**: Clear upgrade documentation
|
||||
|
||||
### Response Format
|
||||
```go
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
```go
|
||||
type PaginatedResponse struct {
|
||||
APIResponse
|
||||
Pagination PaginationResponse `json:"pagination"`
|
||||
}
|
||||
```
|
||||
|
||||
## Middleware Architecture
|
||||
|
||||
### Middleware Stack (Order Matters)
|
||||
1. **Recovery**: Panic recovery and logging
|
||||
2. **Security Headers**: OWASP security headers
|
||||
3. **CORS**: Cross-origin resource sharing
|
||||
4. **Rate Limiting**: Request rate limiting
|
||||
5. **Request Logging**: HTTP request/response logging
|
||||
6. **Authentication**: JWT token validation
|
||||
7. **Authorization**: Permission validation
|
||||
8. **Input Validation**: Request sanitization
|
||||
|
||||
### Security Middleware Components
|
||||
- **Rate Limiter**: Token bucket algorithm with IP-based limits
|
||||
- **Input Sanitizer**: XSS protection, SQL injection prevention
|
||||
- **Content Validator**: Content-type and size validation
|
||||
- **User Agent Validator**: Bot detection and blocking
|
||||
|
||||
## Configuration Architecture
|
||||
|
||||
### Configuration Layers
|
||||
1. **Environment Variables**: Runtime configuration
|
||||
2. **Database Config**: Dynamic configuration storage
|
||||
3. **Default Values**: Fallback configuration
|
||||
4. **Validation**: Configuration validation rules
|
||||
|
||||
### Configuration Categories
|
||||
- **Security**: JWT secrets, encryption keys, password policies
|
||||
- **Database**: Connection settings, migration options
|
||||
- **API**: Rate limits, timeouts, CORS settings
|
||||
- **Logging**: Log levels, retention policies
|
||||
- **Features**: Feature flags, service toggles
|
||||
|
||||
## Error Handling Architecture
|
||||
|
||||
### Error Types
|
||||
1. **Business Errors**: Domain rule violations
|
||||
2. **Validation Errors**: Input validation failures
|
||||
3. **System Errors**: Infrastructure failures
|
||||
4. **Security Errors**: Authentication/authorization failures
|
||||
|
||||
### Error Propagation
|
||||
```
|
||||
Controller → Service → Repository
|
||||
↓ ↓ ↓
|
||||
HTTP Business Data
|
||||
Error Error Error
|
||||
```
|
||||
|
||||
### Error Response Format
|
||||
```go
|
||||
type HTTPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
## Logging Architecture
|
||||
|
||||
### Log Levels
|
||||
- **DEBUG**: Detailed information for debugging
|
||||
- **INFO**: General information about application flow
|
||||
- **WARN**: Warning messages for potential issues
|
||||
- **ERROR**: Error messages for failures
|
||||
- **PANIC**: Critical errors that cause application termination
|
||||
|
||||
### Log Categories
|
||||
1. **Application Logs**: General application flow
|
||||
2. **HTTP Logs**: Request/response logging
|
||||
3. **Security Logs**: Authentication, authorization events
|
||||
4. **Audit Logs**: User actions and system changes
|
||||
5. **Performance Logs**: Response times, resource usage
|
||||
|
||||
### Log Format
|
||||
```
|
||||
[TIMESTAMP] LEVEL [CALLER] MESSAGE
|
||||
[2024-01-01 12:00:00] INFO [main.go:42] Starting application
|
||||
```
|
||||
|
||||
## Caching Architecture
|
||||
|
||||
### Cache Layers
|
||||
1. **Application Cache**: In-memory cache for frequently accessed data
|
||||
2. **Database Cache**: GORM query result caching
|
||||
3. **HTTP Cache**: Response caching for static content
|
||||
|
||||
### Cache Strategies
|
||||
- **TTL-based**: Time-to-live expiration
|
||||
- **LRU**: Least recently used eviction
|
||||
- **Size-based**: Memory usage limits
|
||||
- **Manual**: Explicit cache invalidation
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
### Test Layers
|
||||
1. **Unit Tests**: Individual component testing
|
||||
2. **Integration Tests**: Component interaction testing
|
||||
3. **API Tests**: End-to-end API testing
|
||||
4. **Security Tests**: Security vulnerability testing
|
||||
|
||||
### Test Structure
|
||||
```
|
||||
test/
|
||||
├── unit/ # Unit tests
|
||||
├── integration/ # Integration tests
|
||||
├── api/ # API tests
|
||||
├── fixtures/ # Test data
|
||||
└── helpers/ # Test utilities
|
||||
```
|
||||
|
||||
### Mock Strategy
|
||||
- **Interface Mocking**: Mock external dependencies
|
||||
- **Database Mocking**: In-memory database for tests
|
||||
- **HTTP Mocking**: Mock external API calls
|
||||
|
||||
## Scalability Considerations
|
||||
|
||||
### Horizontal Scaling
|
||||
- **Stateless Design**: No server-side session storage
|
||||
- **Database Connection Pooling**: Efficient connection management
|
||||
- **Load Balancer Friendly**: Health checks and graceful shutdown
|
||||
|
||||
### Performance Optimization
|
||||
- **Database Indexing**: Optimized query performance
|
||||
- **Pagination**: Limit result set sizes
|
||||
- **Caching**: Reduce database load
|
||||
- **Compression**: Reduce bandwidth usage
|
||||
|
||||
### Monitoring Points
|
||||
- **Health Endpoints**: Application health status
|
||||
- **Metrics Collection**: Performance metrics
|
||||
- **Log Aggregation**: Centralized logging
|
||||
- **Alert Configuration**: Automated alerting
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Build Process
|
||||
1. **Dependency Resolution**: `go mod download`
|
||||
2. **Code Generation**: Swagger documentation
|
||||
3. **Compilation**: Static binary creation
|
||||
4. **Testing**: Automated test execution
|
||||
5. **Packaging**: Container image creation
|
||||
|
||||
### Deployment Strategies
|
||||
- **Blue-Green**: Zero-downtime deployments
|
||||
- **Rolling**: Gradual instance replacement
|
||||
- **Canary**: Phased traffic migration
|
||||
- **Feature Flags**: Runtime feature control
|
||||
|
||||
### Environment Configuration
|
||||
- **Development**: Local development setup
|
||||
- **Staging**: Production-like testing environment
|
||||
- **Production**: Live application environment
|
||||
- **DR**: Disaster recovery environment
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
1. **Authentication Bypass**: JWT validation, rate limiting
|
||||
2. **Authorization Escalation**: RBAC, permission validation
|
||||
3. **Data Injection**: Input sanitization, parameterized queries
|
||||
4. **Data Exposure**: Access logging, encryption at rest
|
||||
5. **DoS Attacks**: Rate limiting, request size limits
|
||||
|
||||
### Security Controls
|
||||
- **Input Validation**: All user inputs validated
|
||||
- **Output Encoding**: XSS prevention
|
||||
- **Access Control**: Role-based permissions
|
||||
- **Audit Logging**: All actions logged
|
||||
- **Encryption**: Sensitive data encrypted
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Planned Enhancements
|
||||
- **Microservices**: Service decomposition
|
||||
- **Event Sourcing**: Event-driven architecture
|
||||
- **CQRS**: Command query responsibility segregation
|
||||
- **GraphQL**: Alternative API interface
|
||||
- **Real-time**: WebSocket support
|
||||
|
||||
### Technology Evolution
|
||||
- **Database**: PostgreSQL migration path
|
||||
- **Cache**: Redis integration
|
||||
- **Messaging**: Event bus implementation
|
||||
- **Search**: Full-text search capabilities
|
||||
- **Analytics**: Business intelligence integration
|
||||
505
docs/SECURITY.md
Normal file
505
docs/SECURITY.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# Security Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the comprehensive security measures implemented in the Bootstrap App. The application follows industry best practices and security standards to protect against common threats and vulnerabilities.
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Defense in Depth
|
||||
|
||||
The application implements multiple layers of security:
|
||||
|
||||
1. **Network Security**: HTTPS, CORS, security headers
|
||||
2. **Authentication**: JWT token-based authentication
|
||||
3. **Authorization**: Role-based access control (RBAC)
|
||||
4. **Input Security**: Validation, sanitization, size limits
|
||||
5. **Data Security**: Encryption, secure storage
|
||||
6. **Monitoring**: Audit logging, security event tracking
|
||||
7. **Infrastructure**: Secure deployment practices
|
||||
|
||||
## Authentication System
|
||||
|
||||
### JWT (JSON Web Tokens)
|
||||
|
||||
**Implementation**:
|
||||
- RS256/HS256 signing algorithms
|
||||
- Configurable token expiration (default: 24 hours access, 7 days refresh)
|
||||
- Secure token storage and transmission
|
||||
- Token revocation support
|
||||
|
||||
**Security Features**:
|
||||
- Cryptographically secure secret generation
|
||||
- Token payload minimization
|
||||
- Automatic token expiration
|
||||
- Refresh token rotation
|
||||
|
||||
**Configuration**:
|
||||
```env
|
||||
JWT_SECRET=base64-encoded-secret-64-bytes
|
||||
JWT_ACCESS_TTL_HOURS=24
|
||||
JWT_REFRESH_TTL_DAYS=7
|
||||
JWT_ISSUER=omega-server
|
||||
```
|
||||
|
||||
### Password Security
|
||||
|
||||
**Hashing**:
|
||||
- bcrypt with cost factor 12
|
||||
- Salt automatically generated per password
|
||||
- Resistant to rainbow table attacks
|
||||
|
||||
**Password Policy**:
|
||||
- Minimum 8 characters
|
||||
- Must contain uppercase, lowercase, number, special character
|
||||
- Common password detection
|
||||
- Password strength scoring (0-100)
|
||||
|
||||
**Additional Security**:
|
||||
- Account lockout after failed attempts
|
||||
- Password history prevention
|
||||
- Secure password reset flow
|
||||
|
||||
## Authorization System
|
||||
|
||||
### Role-Based Access Control (RBAC)
|
||||
|
||||
**Components**:
|
||||
- **Users**: Individual user accounts
|
||||
- **Roles**: Collections of permissions (admin, user, viewer)
|
||||
- **Permissions**: Granular access rights (user:create, user:read, etc.)
|
||||
|
||||
**Permission Format**:
|
||||
```
|
||||
resource:action
|
||||
Examples: user:create, role:delete, system:admin
|
||||
```
|
||||
|
||||
**Default Roles**:
|
||||
- **admin**: Full system access
|
||||
- **user**: Standard user privileges
|
||||
- **viewer**: Read-only access
|
||||
|
||||
### Permission Checking
|
||||
|
||||
**Implementation**:
|
||||
```go
|
||||
// Middleware-based permission checking
|
||||
app.Get("/api/v1/users", authMW.HasPermission("user:read"), controller.GetUsers)
|
||||
|
||||
// Service-level permission checking
|
||||
if !user.HasPermission("user:create") {
|
||||
return errors.New("insufficient permissions")
|
||||
}
|
||||
```
|
||||
|
||||
## Input Security
|
||||
|
||||
### Validation & Sanitization
|
||||
|
||||
**Input Validation**:
|
||||
- JSON schema validation
|
||||
- Type checking
|
||||
- Range validation
|
||||
- Format validation (email, UUID, etc.)
|
||||
|
||||
**Input Sanitization**:
|
||||
- HTML entity encoding
|
||||
- SQL injection prevention
|
||||
- XSS protection
|
||||
- Path traversal prevention
|
||||
|
||||
**Request Limits**:
|
||||
- Maximum request size: 10MB
|
||||
- Request timeout: 30 seconds
|
||||
- Header size limits
|
||||
- URL length limits
|
||||
|
||||
### Content Security
|
||||
|
||||
**Content-Type Validation**:
|
||||
- Strict content-type checking
|
||||
- File upload validation
|
||||
- MIME type verification
|
||||
- Extension whitelist
|
||||
|
||||
**File Upload Security**:
|
||||
- Virus scanning integration points
|
||||
- File size limits
|
||||
- Storage isolation
|
||||
- Access control
|
||||
|
||||
## Network Security
|
||||
|
||||
### HTTPS/TLS
|
||||
|
||||
**Requirements**:
|
||||
- TLS 1.2+ minimum
|
||||
- Strong cipher suites
|
||||
- Perfect Forward Secrecy
|
||||
- HSTS headers
|
||||
|
||||
**Certificate Management**:
|
||||
- Automated certificate renewal
|
||||
- Certificate pinning options
|
||||
- Certificate transparency monitoring
|
||||
|
||||
### CORS (Cross-Origin Resource Sharing)
|
||||
|
||||
**Configuration**:
|
||||
```env
|
||||
CORS_ALLOWED_ORIGIN=https://yourdomain.com,https://app.yourdomain.com
|
||||
```
|
||||
|
||||
**Security Headers**:
|
||||
- Strict origin validation
|
||||
- Credential handling
|
||||
- Preflight request validation
|
||||
|
||||
### Security Headers
|
||||
|
||||
**Implemented Headers**:
|
||||
```http
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||||
Content-Security-Policy: default-src 'self'
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Multi-Layer Rate Limiting
|
||||
|
||||
**Global Rate Limiting**:
|
||||
- 100 requests per minute per IP
|
||||
- Configurable limits per endpoint
|
||||
- Token bucket algorithm
|
||||
|
||||
**Authentication Rate Limiting**:
|
||||
- 5 login attempts per 15 minutes
|
||||
- Progressive delays for failed attempts
|
||||
- Account lockout protection
|
||||
|
||||
**API Rate Limiting**:
|
||||
- Per-user rate limits
|
||||
- Per-endpoint specific limits
|
||||
- Burst capacity handling
|
||||
|
||||
### Implementation
|
||||
|
||||
```go
|
||||
// Global rate limiting
|
||||
app.Use(securityMW.RateLimit(100, 1*time.Minute))
|
||||
|
||||
// Authentication rate limiting
|
||||
app.Post("/auth/login", securityMW.AuthRateLimit(), authController.Login)
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
### Encryption
|
||||
|
||||
**Encryption at Rest**:
|
||||
- Database field-level encryption for sensitive data
|
||||
- AES-256-GCM encryption
|
||||
- Secure key management
|
||||
|
||||
**Encryption in Transit**:
|
||||
- TLS 1.2+ for all communications
|
||||
- Certificate validation
|
||||
- Perfect Forward Secrecy
|
||||
|
||||
**Key Management**:
|
||||
```env
|
||||
ENCRYPTION_KEY=32-character-hex-key # 256-bit key
|
||||
```
|
||||
|
||||
### Data Classification
|
||||
|
||||
**Sensitive Data Types**:
|
||||
- Passwords (hashed with bcrypt)
|
||||
- Personal information (encrypted)
|
||||
- Authentication tokens (secure storage)
|
||||
- System secrets (environment variables)
|
||||
|
||||
**Data Handling**:
|
||||
- Minimal data collection
|
||||
- Data retention policies
|
||||
- Secure data deletion
|
||||
- Access logging
|
||||
|
||||
## Audit & Monitoring
|
||||
|
||||
### Audit Logging
|
||||
|
||||
**Logged Events**:
|
||||
- Authentication attempts (success/failure)
|
||||
- Authorization decisions
|
||||
- Data access and modifications
|
||||
- Administrative actions
|
||||
- Security events
|
||||
|
||||
**Audit Log Format**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"userId": "user-id",
|
||||
"action": "create",
|
||||
"resource": "user",
|
||||
"resourceId": "target-user-id",
|
||||
"success": true,
|
||||
"ipAddress": "192.168.1.1",
|
||||
"userAgent": "Browser/1.0",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"details": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### Security Event Monitoring
|
||||
|
||||
**Event Types**:
|
||||
- Brute force attempts
|
||||
- Privilege escalation attempts
|
||||
- Suspicious activity patterns
|
||||
- Rate limit violations
|
||||
- Authentication anomalies
|
||||
|
||||
**Response Actions**:
|
||||
- Automatic account lockout
|
||||
- Rate limit enforcement
|
||||
- Security team notifications
|
||||
- Event correlation
|
||||
|
||||
## Vulnerability Management
|
||||
|
||||
### Security Testing
|
||||
|
||||
**Automated Testing**:
|
||||
- Static code analysis
|
||||
- Dependency vulnerability scanning
|
||||
- Security unit tests
|
||||
- Integration security tests
|
||||
|
||||
**Manual Testing**:
|
||||
- Penetration testing
|
||||
- Code review
|
||||
- Security architecture review
|
||||
- Threat modeling
|
||||
|
||||
### Dependency Management
|
||||
|
||||
**Security Practices**:
|
||||
- Regular dependency updates
|
||||
- Vulnerability scanning
|
||||
- License compliance
|
||||
- Supply chain security
|
||||
|
||||
**Go Module Security**:
|
||||
```bash
|
||||
go mod tidy
|
||||
go list -m -u all
|
||||
go mod download
|
||||
```
|
||||
|
||||
## Incident Response
|
||||
|
||||
### Security Incident Types
|
||||
|
||||
**Authentication Incidents**:
|
||||
- Brute force attacks
|
||||
- Credential stuffing
|
||||
- Account takeover
|
||||
|
||||
**Authorization Incidents**:
|
||||
- Privilege escalation
|
||||
- Unauthorized access
|
||||
- Permission bypass
|
||||
|
||||
**Data Incidents**:
|
||||
- Data breach
|
||||
- Data exposure
|
||||
- Data integrity issues
|
||||
|
||||
### Response Procedures
|
||||
|
||||
**Detection**:
|
||||
- Automated monitoring alerts
|
||||
- Log analysis
|
||||
- User reports
|
||||
- Security team monitoring
|
||||
|
||||
**Response**:
|
||||
1. Incident containment
|
||||
2. Impact assessment
|
||||
3. Evidence preservation
|
||||
4. Stakeholder notification
|
||||
5. Recovery procedures
|
||||
6. Post-incident review
|
||||
|
||||
## Secure Configuration
|
||||
|
||||
### Environment Security
|
||||
|
||||
**Production Settings**:
|
||||
```env
|
||||
GO_ENV=production
|
||||
DEBUG_MODE=false
|
||||
LOG_LEVEL=WARN
|
||||
```
|
||||
|
||||
**Secret Management**:
|
||||
- Environment variable storage
|
||||
- Secure secret generation
|
||||
- Secret rotation procedures
|
||||
- Access control for secrets
|
||||
|
||||
### Database Security
|
||||
|
||||
**Security Measures**:
|
||||
- Connection encryption
|
||||
- Prepared statements (SQL injection prevention)
|
||||
- Access control
|
||||
- Backup encryption
|
||||
|
||||
**Configuration**:
|
||||
- Minimal database privileges
|
||||
- Regular security updates
|
||||
- Connection pooling limits
|
||||
- Query logging for monitoring
|
||||
|
||||
## Compliance & Standards
|
||||
|
||||
### Security Standards
|
||||
|
||||
**Frameworks**:
|
||||
- OWASP Top 10 compliance
|
||||
- NIST Cybersecurity Framework
|
||||
- ISO 27001 principles
|
||||
- PCI DSS guidelines (where applicable)
|
||||
|
||||
**Security Controls**:
|
||||
- Access control (AC)
|
||||
- Audit and accountability (AU)
|
||||
- Configuration management (CM)
|
||||
- Identification and authentication (IA)
|
||||
- System and communications protection (SC)
|
||||
|
||||
### Privacy Protection
|
||||
|
||||
**Data Protection**:
|
||||
- GDPR compliance considerations
|
||||
- Data minimization principles
|
||||
- User consent management
|
||||
- Right to deletion
|
||||
|
||||
**Privacy by Design**:
|
||||
- Default privacy settings
|
||||
- Data encryption
|
||||
- Access logging
|
||||
- User control over data
|
||||
|
||||
## Deployment Security
|
||||
|
||||
### Secure Deployment
|
||||
|
||||
**Build Security**:
|
||||
- Secure build pipeline
|
||||
- Dependency verification
|
||||
- Binary signing
|
||||
- Vulnerability scanning
|
||||
|
||||
**Runtime Security**:
|
||||
- Minimal attack surface
|
||||
- Process isolation
|
||||
- Resource limits
|
||||
- Security monitoring
|
||||
|
||||
### Infrastructure Security
|
||||
|
||||
**Server Hardening**:
|
||||
- OS security updates
|
||||
- Unnecessary service removal
|
||||
- Firewall configuration
|
||||
- Intrusion detection
|
||||
|
||||
**Container Security** (if applicable):
|
||||
- Minimal base images
|
||||
- Security scanning
|
||||
- Runtime protection
|
||||
- Resource limits
|
||||
|
||||
## Security Maintenance
|
||||
|
||||
### Regular Security Tasks
|
||||
|
||||
**Daily**:
|
||||
- Security log review
|
||||
- Incident monitoring
|
||||
- Threat intelligence updates
|
||||
|
||||
**Weekly**:
|
||||
- Vulnerability assessment
|
||||
- Security metric review
|
||||
- Access review
|
||||
|
||||
**Monthly**:
|
||||
- Security training updates
|
||||
- Policy review
|
||||
- Penetration testing
|
||||
- Security architecture review
|
||||
|
||||
### Security Updates
|
||||
|
||||
**Update Process**:
|
||||
1. Security advisory monitoring
|
||||
2. Impact assessment
|
||||
3. Testing in staging
|
||||
4. Coordinated deployment
|
||||
5. Verification testing
|
||||
|
||||
**Emergency Updates**:
|
||||
- Critical vulnerability response
|
||||
- Out-of-band patching
|
||||
- Incident coordination
|
||||
- Communication procedures
|
||||
|
||||
## Security Contact Information
|
||||
|
||||
### Reporting Security Issues
|
||||
|
||||
**Internal Team**:
|
||||
- Security team email: security@company.com
|
||||
- Incident hotline: +1-XXX-XXX-XXXX
|
||||
- Escalation procedures: [Internal documentation]
|
||||
|
||||
**External Researchers**:
|
||||
- Security disclosure policy
|
||||
- Responsible disclosure program
|
||||
- Bug bounty program (if applicable)
|
||||
- PGP key for encrypted communication
|
||||
|
||||
### Security Training
|
||||
|
||||
**Developer Training**:
|
||||
- Secure coding practices
|
||||
- Security testing procedures
|
||||
- Incident response training
|
||||
- Regular security updates
|
||||
|
||||
**User Training**:
|
||||
- Security awareness
|
||||
- Password best practices
|
||||
- Phishing recognition
|
||||
- Incident reporting
|
||||
|
||||
## Conclusion
|
||||
|
||||
This security documentation outlines the comprehensive security measures implemented in the Bootstrap App. Regular review and updates of these security practices ensure ongoing protection against evolving threats.
|
||||
|
||||
For questions about security implementations or to report security issues, please contact the security team through the designated channels.
|
||||
|
||||
**Last Updated**: [Current Date]
|
||||
**Next Review**: [Review Date]
|
||||
**Document Version**: 1.0
|
||||
42
go.mod
Normal file
42
go.mod
Normal file
@@ -0,0 +1,42 @@
|
||||
module omega-server
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.52.8
|
||||
github.com/gofiber/swagger v1.1.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
go.uber.org/dig v1.17.1
|
||||
golang.org/x/crypto v0.39.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer 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/swag v0.23.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
github.com/swaggo/swag v1.16.3 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
88
go.sum
Normal file
88
go.sum
Normal file
@@ -0,0 +1,88 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
|
||||
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ=
|
||||
github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
||||
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/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
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/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
37
local/api/api.go
Normal file
37
local/api/api.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"omega-server/local/controller"
|
||||
"omega-server/local/utl/common"
|
||||
"omega-server/local/utl/configs"
|
||||
"omega-server/local/utl/logging"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// Routes
|
||||
// Initializes web api controllers and its corresponding routes.
|
||||
//
|
||||
// Args:
|
||||
// *fiber.App: Fiber Application
|
||||
func Init(di *dig.Container, app *fiber.App) {
|
||||
|
||||
// Protected routes
|
||||
groups := app.Group(configs.Prefix)
|
||||
|
||||
routeGroups := &common.RouteGroups{
|
||||
API: groups.Group("/api"),
|
||||
Auth: groups.Group("/auth"),
|
||||
Users: groups.Group("/membership"),
|
||||
}
|
||||
|
||||
err := di.Provide(func() *common.RouteGroups {
|
||||
return routeGroups
|
||||
})
|
||||
if err != nil {
|
||||
logging.Panic("unable to bind routes")
|
||||
}
|
||||
|
||||
controller.InitializeControllers(di)
|
||||
}
|
||||
47
local/controller/api.go
Normal file
47
local/controller/api.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"omega-server/local/middleware"
|
||||
"omega-server/local/service"
|
||||
"omega-server/local/utl/common"
|
||||
"omega-server/local/utl/error_handler"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ApiController struct {
|
||||
service *service.ApiService
|
||||
errorHandler *error_handler.ControllerErrorHandler
|
||||
}
|
||||
|
||||
// NewApiController
|
||||
// Initializes ApiController.
|
||||
//
|
||||
// Args:
|
||||
// *services.ApiService: API service
|
||||
// *Fiber.RouterGroup: Fiber Router Group
|
||||
// Returns:
|
||||
// *ApiController: Controller for "api" interactions
|
||||
func NewApiController(as *service.ApiService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *ApiController {
|
||||
ac := &ApiController{
|
||||
service: as,
|
||||
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||
}
|
||||
|
||||
apiGroup := routeGroups.API
|
||||
apiGroup.Use(auth.Authenticate)
|
||||
apiGroup.Get("/", ac.getFirst)
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
// getFirst returns API
|
||||
//
|
||||
// @Summary Return API
|
||||
// @Description Return API
|
||||
// @Tags api
|
||||
// @Success 200 {array} string
|
||||
// @Router /v1/api [get]
|
||||
func (ac *ApiController) getFirst(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
32
local/controller/controller.go
Normal file
32
local/controller/controller.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"omega-server/local/middleware"
|
||||
"omega-server/local/service"
|
||||
"omega-server/local/utl/logging"
|
||||
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// InitializeControllers
|
||||
// Initializes Dependency Injection modules and registers controllers
|
||||
//
|
||||
// Args:
|
||||
// *dig.Container: Dig Container
|
||||
func InitializeControllers(c *dig.Container) {
|
||||
service.InitializeServices(c)
|
||||
|
||||
if err := c.Provide(middleware.NewAuthMiddleware); err != nil {
|
||||
logging.Panic("unable to initialize auth middleware")
|
||||
}
|
||||
|
||||
err := c.Invoke(NewApiController)
|
||||
if err != nil {
|
||||
logging.Panic("unable to initialize api controller")
|
||||
}
|
||||
|
||||
err = c.Invoke(NewMembershipController)
|
||||
if err != nil {
|
||||
logging.Panic("unable to initialize membership controller")
|
||||
}
|
||||
}
|
||||
180
local/controller/membership.go
Normal file
180
local/controller/membership.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"omega-server/local/middleware"
|
||||
"omega-server/local/service"
|
||||
"omega-server/local/utl/common"
|
||||
"omega-server/local/utl/error_handler"
|
||||
"omega-server/local/utl/logging"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MembershipController handles API requests for membership.
|
||||
type MembershipController struct {
|
||||
service *service.MembershipService
|
||||
auth *middleware.AuthMiddleware
|
||||
errorHandler *error_handler.ControllerErrorHandler
|
||||
}
|
||||
|
||||
// NewMembershipController creates a new MembershipController.
|
||||
func NewMembershipController(service *service.MembershipService, auth *middleware.AuthMiddleware, routeGroups *common.RouteGroups) *MembershipController {
|
||||
mc := &MembershipController{
|
||||
service: service,
|
||||
auth: auth,
|
||||
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||
}
|
||||
// Setup initial data for membership
|
||||
if err := service.SetupInitialData(context.Background()); err != nil {
|
||||
logging.Panic(fmt.Sprintf("failed to setup initial data: %v", err))
|
||||
}
|
||||
|
||||
routeGroups.Auth.Post("/login", mc.Login)
|
||||
|
||||
usersGroup := routeGroups.Users
|
||||
usersGroup.Use(mc.auth.Authenticate)
|
||||
usersGroup.Post("/", mc.CreateUser)
|
||||
usersGroup.Get("/", mc.ListUsers)
|
||||
|
||||
usersGroup.Get("/roles", mc.GetRoles)
|
||||
usersGroup.Get("/:id", mc.GetUser)
|
||||
usersGroup.Put("/:id", mc.UpdateUser)
|
||||
usersGroup.Delete("/:id", mc.DeleteUser)
|
||||
|
||||
routeGroups.Auth.Get("/me", mc.auth.Authenticate, mc.GetMe)
|
||||
|
||||
return mc
|
||||
}
|
||||
|
||||
// Login handles user login.
|
||||
func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
||||
type request struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var req request
|
||||
if err := ctx.BodyParser(&req); err != nil {
|
||||
return c.errorHandler.HandleParsingError(ctx, err)
|
||||
}
|
||||
|
||||
logging.Debug("Login request received")
|
||||
token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
return c.errorHandler.HandleAuthError(ctx, err)
|
||||
}
|
||||
|
||||
return ctx.JSON(fiber.Map{"token": token})
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
|
||||
type request struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
var req request
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return mc.errorHandler.HandleParsingError(c, err)
|
||||
}
|
||||
|
||||
user, err := mc.service.CreateUser(c.UserContext(), req.Username, req.Password, req.Role)
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
// ListUsers lists all users.
|
||||
func (mc *MembershipController) ListUsers(c *fiber.Ctx) error {
|
||||
users, err := mc.service.ListUsers(c.UserContext())
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(users)
|
||||
}
|
||||
|
||||
// GetUser gets a single user by ID.
|
||||
func (mc *MembershipController) GetUser(c *fiber.Ctx) error {
|
||||
id, err := uuid.Parse(c.Params("id"))
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleUUIDError(c, "user ID")
|
||||
}
|
||||
|
||||
user, err := mc.service.GetUser(c.UserContext(), id)
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleNotFoundError(c, "User")
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
// GetMe returns the currently authenticated user's details.
|
||||
func (mc *MembershipController) GetMe(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(string)
|
||||
if !ok || userID == "" {
|
||||
return mc.errorHandler.HandleAuthError(c, fmt.Errorf("unauthorized: user ID not found in context"))
|
||||
}
|
||||
|
||||
user, err := mc.service.GetUserWithPermissions(c.UserContext(), userID)
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleNotFoundError(c, "User")
|
||||
}
|
||||
|
||||
// Sanitize the user object to not expose password
|
||||
user.PasswordHash = ""
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user.
|
||||
func (mc *MembershipController) DeleteUser(c *fiber.Ctx) error {
|
||||
id, err := uuid.Parse(c.Params("id"))
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleUUIDError(c, "user ID")
|
||||
}
|
||||
|
||||
err = mc.service.DeleteUser(c.UserContext(), id)
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateUser updates a user.
|
||||
func (mc *MembershipController) UpdateUser(c *fiber.Ctx) error {
|
||||
id, err := uuid.Parse(c.Params("id"))
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleUUIDError(c, "user ID")
|
||||
}
|
||||
|
||||
var req service.UpdateUserRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return mc.errorHandler.HandleParsingError(c, err)
|
||||
}
|
||||
|
||||
user, err := mc.service.UpdateUser(c.UserContext(), id, req)
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
// GetRoles returns all available roles.
|
||||
func (mc *MembershipController) GetRoles(c *fiber.Ctx) error {
|
||||
roles, err := mc.service.GetAllRoles(c.UserContext())
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(roles)
|
||||
}
|
||||
243
local/middleware/auth.go
Normal file
243
local/middleware/auth.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"omega-server/local/middleware/security"
|
||||
"omega-server/local/service"
|
||||
"omega-server/local/utl/cache"
|
||||
"omega-server/local/utl/jwt"
|
||||
"omega-server/local/utl/logging"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// CachedUserInfo holds cached user authentication and permission data
|
||||
type CachedUserInfo struct {
|
||||
UserID string
|
||||
Username string
|
||||
Roles []string
|
||||
RoleNames []string
|
||||
Permissions map[string]bool
|
||||
CachedAt time.Time
|
||||
}
|
||||
|
||||
// AuthMiddleware provides authentication and permission middleware.
|
||||
type AuthMiddleware struct {
|
||||
membershipService service.MembershipServiceInterface
|
||||
cache *cache.InMemoryCache
|
||||
securityMW *security.SecurityMiddleware
|
||||
}
|
||||
|
||||
// NewAuthMiddleware creates a new AuthMiddleware.
|
||||
func NewAuthMiddleware(ms service.MembershipServiceInterface, cache *cache.InMemoryCache) *AuthMiddleware {
|
||||
auth := &AuthMiddleware{
|
||||
membershipService: ms,
|
||||
cache: cache,
|
||||
securityMW: security.NewSecurityMiddleware(),
|
||||
}
|
||||
|
||||
// Set up bidirectional relationship for cache invalidation
|
||||
ms.SetCacheInvalidator(auth)
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
// Authenticate is a middleware for JWT authentication with enhanced security.
|
||||
func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
|
||||
// Log authentication attempt
|
||||
ip := ctx.IP()
|
||||
userAgent := ctx.Get("User-Agent")
|
||||
|
||||
authHeader := ctx.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
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, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
logging.Error("Authentication failed: malformed Authorization header from IP %s", ip)
|
||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Missing or malformed JWT",
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
// Preload and cache user info to avoid database queries on permission checks
|
||||
userInfo, err := m.getCachedUserInfo(ctx.UserContext(), claims.UserID)
|
||||
if err != nil {
|
||||
logging.Error("Authentication failed: unable to load user info for %s from IP %s: %v", claims.UserID, ip, err)
|
||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Invalid or expired JWT",
|
||||
})
|
||||
}
|
||||
|
||||
ctx.Locals("userID", claims.UserID)
|
||||
ctx.Locals("userInfo", userInfo)
|
||||
ctx.Locals("authTime", time.Now())
|
||||
|
||||
logging.InfoWithContext("AUTH", "User %s authenticated successfully from IP %s", claims.UserID, ip)
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
// HasPermission is a middleware for checking user permissions with enhanced logging.
|
||||
func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler {
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
userID, ok := ctx.Locals("userID").(string)
|
||||
if !ok {
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
// Use cached user info from authentication step - no database queries needed
|
||||
userInfo, ok := ctx.Locals("userInfo").(*CachedUserInfo)
|
||||
if !ok {
|
||||
logging.Error("Permission check failed: no cached user info in context from IP %s", ctx.IP())
|
||||
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if user has permission using cached data
|
||||
has := m.hasPermissionFromCache(userInfo, requiredPermission)
|
||||
|
||||
if !has {
|
||||
logging.WarnWithContext("AUTH", "Permission denied: user %s lacks permission %s, IP %s", userID, requiredPermission, ctx.IP())
|
||||
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "Forbidden",
|
||||
})
|
||||
}
|
||||
|
||||
logging.DebugWithContext("AUTH", "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()
|
||||
}
|
||||
}
|
||||
|
||||
// getCachedUserInfo retrieves and caches complete user information including permissions
|
||||
func (m *AuthMiddleware) getCachedUserInfo(ctx context.Context, userID string) (*CachedUserInfo, error) {
|
||||
cacheKey := fmt.Sprintf("userinfo:%s", userID)
|
||||
|
||||
// Try cache first
|
||||
if cached, found := m.cache.Get(cacheKey); found {
|
||||
if userInfo, ok := cached.(*CachedUserInfo); ok {
|
||||
logging.DebugWithContext("AUTH_CACHE", "User info for %s found in cache", userID)
|
||||
return userInfo, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss - load from database
|
||||
user, err := m.membershipService.GetUserWithPermissions(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build permission map for fast lookups
|
||||
permissions := make(map[string]bool)
|
||||
roleNames := make([]string, len(user.Roles))
|
||||
for i, role := range user.Roles {
|
||||
roleNames[i] = role.Name
|
||||
for _, permission := range role.Permissions {
|
||||
permissions[permission.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
userInfo := &CachedUserInfo{
|
||||
UserID: userID,
|
||||
Username: user.Username,
|
||||
Roles: roleNames,
|
||||
RoleNames: roleNames,
|
||||
Permissions: permissions,
|
||||
CachedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Cache for 15 minutes
|
||||
m.cache.Set(cacheKey, userInfo, 15*time.Minute)
|
||||
logging.DebugWithContext("AUTH_CACHE", "User info for %s cached with %d permissions", userID, len(permissions))
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// hasPermissionFromCache checks permissions using cached user info (no database queries)
|
||||
func (m *AuthMiddleware) hasPermissionFromCache(userInfo *CachedUserInfo, permission string) bool {
|
||||
// Super Admin and Admin have all permissions
|
||||
for _, roleName := range userInfo.RoleNames {
|
||||
if roleName == "Super Admin" || roleName == "Admin" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check specific permission in cached map
|
||||
return userInfo.Permissions[permission]
|
||||
}
|
||||
|
||||
// InvalidateUserPermissions removes cached user info for a user
|
||||
func (m *AuthMiddleware) InvalidateUserPermissions(userID string) {
|
||||
cacheKey := fmt.Sprintf("userinfo:%s", userID)
|
||||
m.cache.Delete(cacheKey)
|
||||
logging.InfoWithContext("AUTH_CACHE", "User info cache invalidated for user %s", userID)
|
||||
}
|
||||
|
||||
// InvalidateAllUserPermissions clears all cached user info (useful for role/permission changes)
|
||||
func (m *AuthMiddleware) InvalidateAllUserPermissions() {
|
||||
// This would need to be implemented based on your cache interface
|
||||
// For now, just log that invalidation was requested
|
||||
logging.InfoWithContext("AUTH_CACHE", "All user info caches invalidation requested")
|
||||
}
|
||||
28
local/middleware/auth_test_exports.go
Normal file
28
local/middleware/auth_test_exports.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package middleware
|
||||
|
||||
import "context"
|
||||
|
||||
// ExportedCachedUserInfo exports CachedUserInfo for testing
|
||||
type ExportedCachedUserInfo = CachedUserInfo
|
||||
|
||||
// GetCachedUserInfo exports the internal getCachedUserInfo method for testing
|
||||
func (m *AuthMiddleware) GetCachedUserInfo(ctx context.Context, userID string) (*CachedUserInfo, error) {
|
||||
return m.getCachedUserInfo(ctx, userID)
|
||||
}
|
||||
|
||||
// TestExports provides test-only functionality for AuthMiddleware
|
||||
type TestExports struct {
|
||||
AuthMiddleware *AuthMiddleware
|
||||
}
|
||||
|
||||
// NewTestExports creates a new TestExports instance
|
||||
func NewTestExports(auth *AuthMiddleware) *TestExports {
|
||||
return &TestExports{
|
||||
AuthMiddleware: auth,
|
||||
}
|
||||
}
|
||||
|
||||
// HasPermissionFromCache exports the internal hasPermissionFromCache method for testing
|
||||
func (t *TestExports) HasPermissionFromCache(userInfo *CachedUserInfo, permission string) bool {
|
||||
return t.AuthMiddleware.hasPermissionFromCache(userInfo, permission)
|
||||
}
|
||||
69
local/middleware/logging/request_logging.go
Normal file
69
local/middleware/logging/request_logging.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"omega-server/local/utl/logging"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// RequestLoggingMiddleware logs HTTP requests and responses
|
||||
type RequestLoggingMiddleware struct {
|
||||
infoLogger *logging.InfoLogger
|
||||
}
|
||||
|
||||
// NewRequestLoggingMiddleware creates a new request logging middleware
|
||||
func NewRequestLoggingMiddleware() *RequestLoggingMiddleware {
|
||||
return &RequestLoggingMiddleware{
|
||||
infoLogger: logging.GetInfoLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// Handler returns the middleware handler function
|
||||
func (rlm *RequestLoggingMiddleware) Handler() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Record start time
|
||||
start := time.Now()
|
||||
|
||||
// Log incoming request
|
||||
userAgent := c.Get("User-Agent")
|
||||
if userAgent == "" {
|
||||
userAgent = "Unknown"
|
||||
}
|
||||
|
||||
rlm.infoLogger.LogRequest(c.Method(), c.OriginalURL(), userAgent)
|
||||
|
||||
// Continue to next handler
|
||||
err := c.Next()
|
||||
|
||||
// Calculate duration
|
||||
duration := time.Since(start)
|
||||
|
||||
// Log response
|
||||
statusCode := c.Response().StatusCode()
|
||||
rlm.infoLogger.LogResponse(c.Method(), c.OriginalURL(), statusCode, duration.String())
|
||||
|
||||
// Log error if present
|
||||
if err != nil {
|
||||
logging.ErrorWithContext("REQUEST_MIDDLEWARE", "Request failed: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Global request logging middleware instance
|
||||
var globalRequestLoggingMiddleware *RequestLoggingMiddleware
|
||||
|
||||
// GetRequestLoggingMiddleware returns the global request logging middleware
|
||||
func GetRequestLoggingMiddleware() *RequestLoggingMiddleware {
|
||||
if globalRequestLoggingMiddleware == nil {
|
||||
globalRequestLoggingMiddleware = NewRequestLoggingMiddleware()
|
||||
}
|
||||
return globalRequestLoggingMiddleware
|
||||
}
|
||||
|
||||
// Handler returns the global request logging middleware handler
|
||||
func Handler() fiber.Handler {
|
||||
return GetRequestLoggingMiddleware().Handler()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
271
local/model/audit_log.go
Normal file
271
local/model/audit_log.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuditLog represents an audit log entry in the system
|
||||
type AuditLog struct {
|
||||
BaseModel
|
||||
UserID string `json:"userId" gorm:"type:varchar(36);index"`
|
||||
Action string `json:"action" gorm:"not null;type:varchar(100);index"`
|
||||
Resource string `json:"resource" gorm:"not null;type:varchar(100);index"`
|
||||
ResourceID string `json:"resourceId" gorm:"type:varchar(36);index"`
|
||||
Details map[string]interface{} `json:"details" gorm:"type:text"`
|
||||
IPAddress string `json:"ipAddress" gorm:"type:varchar(45)"`
|
||||
UserAgent string `json:"userAgent" gorm:"type:text"`
|
||||
Success bool `json:"success" gorm:"default:true;index"`
|
||||
ErrorMsg string `json:"errorMsg,omitempty" gorm:"type:text"`
|
||||
Duration int64 `json:"duration,omitempty"` // Duration in milliseconds
|
||||
SessionID string `json:"sessionId,omitempty" gorm:"type:varchar(255)"`
|
||||
RequestID string `json:"requestId,omitempty" gorm:"type:varchar(255)"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// AuditLogCreateRequest represents the request to create a new audit log
|
||||
type AuditLogCreateRequest struct {
|
||||
UserID string `json:"userId"`
|
||||
Action string `json:"action" validate:"required,max=100"`
|
||||
Resource string `json:"resource" validate:"required,max=100"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ipAddress" validate:"max=45"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
Duration int64 `json:"duration"`
|
||||
SessionID string `json:"sessionId"`
|
||||
RequestID string `json:"requestId"`
|
||||
}
|
||||
|
||||
// AuditLogInfo represents public audit log information
|
||||
type AuditLogInfo struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
UserEmail string `json:"userEmail,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ipAddress"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMsg string `json:"errorMsg,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating an audit log
|
||||
func (al *AuditLog) BeforeCreate(tx *gorm.DB) error {
|
||||
al.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize fields
|
||||
al.Action = strings.ToLower(strings.TrimSpace(al.Action))
|
||||
al.Resource = strings.ToLower(strings.TrimSpace(al.Resource))
|
||||
al.IPAddress = strings.TrimSpace(al.IPAddress)
|
||||
al.UserAgent = strings.TrimSpace(al.UserAgent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDetails sets the details field from a map
|
||||
func (al *AuditLog) SetDetails(details map[string]interface{}) error {
|
||||
al.Details = details
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDetails returns the details as a map
|
||||
func (al *AuditLog) GetDetails() map[string]interface{} {
|
||||
if al.Details == nil {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
return al.Details
|
||||
}
|
||||
|
||||
// SetDetailsFromJSON sets the details field from a JSON string
|
||||
func (al *AuditLog) SetDetailsFromJSON(jsonStr string) error {
|
||||
if jsonStr == "" {
|
||||
al.Details = make(map[string]interface{})
|
||||
return nil
|
||||
}
|
||||
|
||||
var details map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &details); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
al.Details = details
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDetailsAsJSON returns the details as a JSON string
|
||||
func (al *AuditLog) GetDetailsAsJSON() (string, error) {
|
||||
if al.Details == nil || len(al.Details) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(al.Details)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// ToAuditLogInfo converts AuditLog to AuditLogInfo (public information)
|
||||
func (al *AuditLog) ToAuditLogInfo() AuditLogInfo {
|
||||
info := AuditLogInfo{
|
||||
ID: al.ID,
|
||||
UserID: al.UserID,
|
||||
Action: al.Action,
|
||||
Resource: al.Resource,
|
||||
ResourceID: al.ResourceID,
|
||||
Details: al.GetDetails(),
|
||||
IPAddress: al.IPAddress,
|
||||
UserAgent: al.UserAgent,
|
||||
Success: al.Success,
|
||||
ErrorMsg: al.ErrorMsg,
|
||||
Duration: al.Duration,
|
||||
SessionID: al.SessionID,
|
||||
RequestID: al.RequestID,
|
||||
DateCreated: al.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
// Include user information if available
|
||||
if al.User != nil {
|
||||
info.UserEmail = al.User.Email
|
||||
info.UserName = al.User.Name
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// AddDetail adds a single detail to the details map
|
||||
func (al *AuditLog) AddDetail(key string, value interface{}) {
|
||||
if al.Details == nil {
|
||||
al.Details = make(map[string]interface{})
|
||||
}
|
||||
al.Details[key] = value
|
||||
}
|
||||
|
||||
// GetDetail gets a single detail from the details map
|
||||
func (al *AuditLog) GetDetail(key string) (interface{}, bool) {
|
||||
if al.Details == nil {
|
||||
return nil, false
|
||||
}
|
||||
value, exists := al.Details[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// Common audit log actions
|
||||
const (
|
||||
AuditActionCreate = "create"
|
||||
AuditActionRead = "read"
|
||||
AuditActionUpdate = "update"
|
||||
AuditActionDelete = "delete"
|
||||
AuditActionLogin = "login"
|
||||
AuditActionLogout = "logout"
|
||||
AuditActionAccess = "access"
|
||||
AuditActionExport = "export"
|
||||
AuditActionImport = "import"
|
||||
AuditActionConfig = "config"
|
||||
)
|
||||
|
||||
// Common audit log resources
|
||||
const (
|
||||
AuditResourceUser = "user"
|
||||
AuditResourceRole = "role"
|
||||
AuditResourcePermission = "permission"
|
||||
AuditResourceSystemConfig = "system_config"
|
||||
AuditResourceAuth = "auth"
|
||||
AuditResourceAPI = "api"
|
||||
AuditResourceFile = "file"
|
||||
AuditResourceDatabase = "database"
|
||||
AuditResourceSystem = "system"
|
||||
)
|
||||
|
||||
// CreateAuditLog creates a new audit log entry
|
||||
func CreateAuditLog(userID, action, resource, resourceID string, success bool) *AuditLog {
|
||||
auditLog := &AuditLog{
|
||||
UserID: userID,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
Success: success,
|
||||
Details: make(map[string]interface{}),
|
||||
}
|
||||
auditLog.Init()
|
||||
return auditLog
|
||||
}
|
||||
|
||||
// CreateAuditLogWithDetails creates a new audit log entry with details
|
||||
func CreateAuditLogWithDetails(userID, action, resource, resourceID string, success bool, details map[string]interface{}) *AuditLog {
|
||||
auditLog := CreateAuditLog(userID, action, resource, resourceID, success)
|
||||
auditLog.Details = details
|
||||
return auditLog
|
||||
}
|
||||
|
||||
// CreateAuditLogWithError creates a new audit log entry for an error
|
||||
func CreateAuditLogWithError(userID, action, resource, resourceID, errorMsg string) *AuditLog {
|
||||
auditLog := CreateAuditLog(userID, action, resource, resourceID, false)
|
||||
auditLog.ErrorMsg = errorMsg
|
||||
return auditLog
|
||||
}
|
||||
|
||||
// SetRequestInfo sets request-related information
|
||||
func (al *AuditLog) SetRequestInfo(ipAddress, userAgent, sessionID, requestID string) {
|
||||
al.IPAddress = ipAddress
|
||||
al.UserAgent = userAgent
|
||||
al.SessionID = sessionID
|
||||
al.RequestID = requestID
|
||||
}
|
||||
|
||||
// SetDuration sets the operation duration
|
||||
func (al *AuditLog) SetDuration(start time.Time) {
|
||||
al.Duration = time.Since(start).Milliseconds()
|
||||
}
|
||||
|
||||
// IsSuccess returns whether the audit log represents a successful operation
|
||||
func (al *AuditLog) IsSuccess() bool {
|
||||
return al.Success
|
||||
}
|
||||
|
||||
// IsFailure returns whether the audit log represents a failed operation
|
||||
func (al *AuditLog) IsFailure() bool {
|
||||
return !al.Success
|
||||
}
|
||||
|
||||
// GetActionDescription returns a human-readable description of the action
|
||||
func (al *AuditLog) GetActionDescription() string {
|
||||
switch al.Action {
|
||||
case AuditActionCreate:
|
||||
return "Created " + al.Resource
|
||||
case AuditActionRead:
|
||||
return "Viewed " + al.Resource
|
||||
case AuditActionUpdate:
|
||||
return "Updated " + al.Resource
|
||||
case AuditActionDelete:
|
||||
return "Deleted " + al.Resource
|
||||
case AuditActionLogin:
|
||||
return "Logged in"
|
||||
case AuditActionLogout:
|
||||
return "Logged out"
|
||||
case AuditActionAccess:
|
||||
return "Accessed " + al.Resource
|
||||
case AuditActionExport:
|
||||
return "Exported " + al.Resource
|
||||
case AuditActionImport:
|
||||
return "Imported " + al.Resource
|
||||
case AuditActionConfig:
|
||||
return "Configured " + al.Resource
|
||||
default:
|
||||
return strings.Title(al.Action) + " " + al.Resource
|
||||
}
|
||||
}
|
||||
110
local/model/base.go
Normal file
110
local/model/base.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BaseModel provides common fields for all database models
|
||||
type BaseModel struct {
|
||||
ID string `json:"id" gorm:"primary_key;type:varchar(36)"`
|
||||
DateCreated time.Time `json:"dateCreated" gorm:"not null"`
|
||||
DateUpdated time.Time `json:"dateUpdated" gorm:"not null"`
|
||||
}
|
||||
|
||||
// Init initializes base model with DateCreated, DateUpdated, and ID values
|
||||
func (bm *BaseModel) Init() {
|
||||
now := time.Now().UTC()
|
||||
bm.ID = uuid.NewString()
|
||||
bm.DateCreated = now
|
||||
bm.DateUpdated = now
|
||||
}
|
||||
|
||||
// UpdateTimestamp updates the DateUpdated field
|
||||
func (bm *BaseModel) UpdateTimestamp() {
|
||||
bm.DateUpdated = time.Now().UTC()
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating a record
|
||||
func (bm *BaseModel) BeforeCreate() error {
|
||||
if bm.ID == "" {
|
||||
bm.Init()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate is a GORM hook that runs before updating a record
|
||||
func (bm *BaseModel) BeforeUpdate() error {
|
||||
bm.UpdateTimestamp()
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilteredResponse represents a paginated response
|
||||
type FilteredResponse struct {
|
||||
Items interface{} `json:"items"`
|
||||
Params
|
||||
}
|
||||
|
||||
// MessageResponse represents a simple message response
|
||||
type MessageResponse struct {
|
||||
Message string `json:"message"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// Params represents pagination and filtering parameters
|
||||
type Params struct {
|
||||
SortBy string `json:"sortBy" query:"sortBy"`
|
||||
SortOrder string `json:"sortOrder" query:"sortOrder"`
|
||||
Page int `json:"page" query:"page"`
|
||||
Limit int `json:"limit" query:"limit"`
|
||||
Search string `json:"search" query:"search"`
|
||||
Filter string `json:"filter" query:"filter"`
|
||||
TotalRecords int64 `json:"totalRecords"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
}
|
||||
|
||||
// DefaultParams returns default pagination parameters
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
SortBy: "dateCreated",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates and sets default values for parameters
|
||||
func (p *Params) Validate() {
|
||||
if p.Page < 1 {
|
||||
p.Page = 1
|
||||
}
|
||||
if p.Limit < 1 || p.Limit > 100 {
|
||||
p.Limit = 10
|
||||
}
|
||||
if p.SortBy == "" {
|
||||
p.SortBy = "dateCreated"
|
||||
}
|
||||
if p.SortOrder != "asc" && p.SortOrder != "desc" {
|
||||
p.SortOrder = "desc"
|
||||
}
|
||||
}
|
||||
|
||||
// Offset calculates the offset for database queries
|
||||
func (p *Params) Offset() int {
|
||||
return (p.Page - 1) * p.Limit
|
||||
}
|
||||
|
||||
// CalculateTotalPages calculates total pages based on total records
|
||||
func (p *Params) CalculateTotalPages() {
|
||||
if p.Limit > 0 {
|
||||
p.TotalPages = int((p.TotalRecords + int64(p.Limit) - 1) / int64(p.Limit))
|
||||
}
|
||||
}
|
||||
228
local/model/membership_filter.go
Normal file
228
local/model/membership_filter.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MembershipFilter represents filters for membership-related queries
|
||||
type MembershipFilter struct {
|
||||
Params
|
||||
// User-specific filters
|
||||
Username string `json:"username" query:"username"`
|
||||
Email string `json:"email" query:"email"`
|
||||
Active *bool `json:"active" query:"active"`
|
||||
EmailVerified *bool `json:"emailVerified" query:"emailVerified"`
|
||||
RoleID string `json:"roleId" query:"roleId"`
|
||||
RoleName string `json:"roleName" query:"roleName"`
|
||||
|
||||
// Role-specific filters
|
||||
RoleActive *bool `json:"roleActive" query:"roleActive"`
|
||||
RoleSystem *bool `json:"roleSystem" query:"roleSystem"`
|
||||
|
||||
// Permission-specific filters
|
||||
PermissionName string `json:"permissionName" query:"permissionName"`
|
||||
PermissionCategory string `json:"permissionCategory" query:"permissionCategory"`
|
||||
PermissionActive *bool `json:"permissionActive" query:"permissionActive"`
|
||||
PermissionSystem *bool `json:"permissionSystem" query:"permissionSystem"`
|
||||
|
||||
// Date range filters
|
||||
CreatedAfter string `json:"createdAfter" query:"createdAfter"`
|
||||
CreatedBefore string `json:"createdBefore" query:"createdBefore"`
|
||||
UpdatedAfter string `json:"updatedAfter" query:"updatedAfter"`
|
||||
UpdatedBefore string `json:"updatedBefore" query:"updatedBefore"`
|
||||
|
||||
// Relationship filters
|
||||
WithRoles bool `json:"withRoles" query:"withRoles"`
|
||||
WithPermissions bool `json:"withPermissions" query:"withPermissions"`
|
||||
WithUsers bool `json:"withUsers" query:"withUsers"`
|
||||
}
|
||||
|
||||
// ApplyFilter applies the filter conditions to a GORM query
|
||||
func (f *MembershipFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
|
||||
if f == nil {
|
||||
return query
|
||||
}
|
||||
|
||||
// Apply search across multiple fields
|
||||
if f.Search != "" {
|
||||
query = query.Where("username LIKE ? OR email LIKE ? OR name LIKE ?",
|
||||
"%"+f.Search+"%", "%"+f.Search+"%", "%"+f.Search+"%")
|
||||
}
|
||||
|
||||
// User-specific filters
|
||||
if f.Username != "" {
|
||||
query = query.Where("username = ?", f.Username)
|
||||
}
|
||||
|
||||
if f.Email != "" {
|
||||
query = query.Where("email = ?", f.Email)
|
||||
}
|
||||
|
||||
if f.Active != nil {
|
||||
query = query.Where("active = ?", *f.Active)
|
||||
}
|
||||
|
||||
if f.EmailVerified != nil {
|
||||
query = query.Where("email_verified = ?", *f.EmailVerified)
|
||||
}
|
||||
|
||||
if f.RoleID != "" {
|
||||
query = query.Joins("JOIN user_roles ON users.id = user_roles.user_id").
|
||||
Where("user_roles.role_id = ?", f.RoleID)
|
||||
}
|
||||
|
||||
if f.RoleName != "" {
|
||||
query = query.Joins("JOIN user_roles ON users.id = user_roles.user_id").
|
||||
Joins("JOIN roles ON user_roles.role_id = roles.id").
|
||||
Where("roles.name = ?", f.RoleName)
|
||||
}
|
||||
|
||||
// Role-specific filters
|
||||
if f.RoleActive != nil {
|
||||
query = query.Where("active = ?", *f.RoleActive)
|
||||
}
|
||||
|
||||
if f.RoleSystem != nil {
|
||||
query = query.Where("system = ?", *f.RoleSystem)
|
||||
}
|
||||
|
||||
// Permission-specific filters
|
||||
if f.PermissionName != "" {
|
||||
query = query.Where("name = ?", f.PermissionName)
|
||||
}
|
||||
|
||||
if f.PermissionCategory != "" {
|
||||
query = query.Where("category = ?", f.PermissionCategory)
|
||||
}
|
||||
|
||||
if f.PermissionActive != nil {
|
||||
query = query.Where("active = ?", *f.PermissionActive)
|
||||
}
|
||||
|
||||
if f.PermissionSystem != nil {
|
||||
query = query.Where("system = ?", *f.PermissionSystem)
|
||||
}
|
||||
|
||||
// Date range filters
|
||||
if f.CreatedAfter != "" {
|
||||
query = query.Where("date_created >= ?", f.CreatedAfter)
|
||||
}
|
||||
|
||||
if f.CreatedBefore != "" {
|
||||
query = query.Where("date_created <= ?", f.CreatedBefore)
|
||||
}
|
||||
|
||||
if f.UpdatedAfter != "" {
|
||||
query = query.Where("date_updated >= ?", f.UpdatedAfter)
|
||||
}
|
||||
|
||||
if f.UpdatedBefore != "" {
|
||||
query = query.Where("date_updated <= ?", f.UpdatedBefore)
|
||||
}
|
||||
|
||||
// Relationship preloading
|
||||
if f.WithRoles {
|
||||
query = query.Preload("Roles")
|
||||
}
|
||||
|
||||
if f.WithPermissions {
|
||||
query = query.Preload("Permissions")
|
||||
}
|
||||
|
||||
if f.WithUsers {
|
||||
query = query.Preload("Users")
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// Pagination returns the offset and limit for pagination
|
||||
func (f *MembershipFilter) Pagination() (offset, limit int) {
|
||||
if f == nil {
|
||||
return 0, 10
|
||||
}
|
||||
|
||||
f.Validate()
|
||||
return f.Offset(), f.Limit
|
||||
}
|
||||
|
||||
// GetSorting returns the sorting field and direction
|
||||
func (f *MembershipFilter) GetSorting() (field string, desc bool) {
|
||||
if f == nil {
|
||||
return "date_created", true
|
||||
}
|
||||
|
||||
f.Validate()
|
||||
|
||||
// Map common sort fields to database column names
|
||||
switch f.SortBy {
|
||||
case "dateCreated":
|
||||
field = "date_created"
|
||||
case "dateUpdated":
|
||||
field = "date_updated"
|
||||
case "username":
|
||||
field = "username"
|
||||
case "email":
|
||||
field = "email"
|
||||
case "name":
|
||||
field = "name"
|
||||
case "active":
|
||||
field = "active"
|
||||
default:
|
||||
field = "date_created"
|
||||
}
|
||||
|
||||
desc = f.SortOrder == "desc"
|
||||
return field, desc
|
||||
}
|
||||
|
||||
// NewMembershipFilter creates a new MembershipFilter with default values
|
||||
func NewMembershipFilter() *MembershipFilter {
|
||||
return &MembershipFilter{
|
||||
Params: DefaultParams(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetUserFilters sets user-specific filters
|
||||
func (f *MembershipFilter) SetUserFilters(username, email string, active, emailVerified *bool) *MembershipFilter {
|
||||
f.Username = username
|
||||
f.Email = email
|
||||
f.Active = active
|
||||
f.EmailVerified = emailVerified
|
||||
return f
|
||||
}
|
||||
|
||||
// SetRoleFilters sets role-specific filters
|
||||
func (f *MembershipFilter) SetRoleFilters(roleID, roleName string, roleActive, roleSystem *bool) *MembershipFilter {
|
||||
f.RoleID = roleID
|
||||
f.RoleName = roleName
|
||||
f.RoleActive = roleActive
|
||||
f.RoleSystem = roleSystem
|
||||
return f
|
||||
}
|
||||
|
||||
// SetPermissionFilters sets permission-specific filters
|
||||
func (f *MembershipFilter) SetPermissionFilters(name, category string, active, system *bool) *MembershipFilter {
|
||||
f.PermissionName = name
|
||||
f.PermissionCategory = category
|
||||
f.PermissionActive = active
|
||||
f.PermissionSystem = system
|
||||
return f
|
||||
}
|
||||
|
||||
// SetDateRangeFilters sets date range filters
|
||||
func (f *MembershipFilter) SetDateRangeFilters(createdAfter, createdBefore, updatedAfter, updatedBefore string) *MembershipFilter {
|
||||
f.CreatedAfter = createdAfter
|
||||
f.CreatedBefore = createdBefore
|
||||
f.UpdatedAfter = updatedAfter
|
||||
f.UpdatedBefore = updatedBefore
|
||||
return f
|
||||
}
|
||||
|
||||
// SetPreloads sets which relationships to preload
|
||||
func (f *MembershipFilter) SetPreloads(withRoles, withPermissions, withUsers bool) *MembershipFilter {
|
||||
f.WithRoles = withRoles
|
||||
f.WithPermissions = withPermissions
|
||||
f.WithUsers = withUsers
|
||||
return f
|
||||
}
|
||||
276
local/model/permission.go
Normal file
276
local/model/permission.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Permission represents a permission in the system
|
||||
type Permission struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"unique;not null;type:varchar(100)"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Category string `json:"category" gorm:"type:varchar(50)"`
|
||||
Active bool `json:"active" gorm:"default:true"`
|
||||
System bool `json:"system" gorm:"default:false"` // System permissions cannot be deleted
|
||||
Roles []Role `json:"-" gorm:"many2many:role_permissions;"`
|
||||
}
|
||||
|
||||
// PermissionCreateRequest represents the request to create a new permission
|
||||
type PermissionCreateRequest struct {
|
||||
Name string `json:"name" validate:"required,min=3,max=100"`
|
||||
Description string `json:"description" validate:"max=500"`
|
||||
Category string `json:"category" validate:"required,max=50"`
|
||||
}
|
||||
|
||||
// PermissionUpdateRequest represents the request to update a permission
|
||||
type PermissionUpdateRequest struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=100"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty,max=500"`
|
||||
Category *string `json:"category,omitempty" validate:"omitempty,max=50"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
}
|
||||
|
||||
// PermissionInfo represents public permission information
|
||||
type PermissionInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Active bool `json:"active"`
|
||||
System bool `json:"system"`
|
||||
RoleCount int64 `json:"roleCount"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a permission
|
||||
func (p *Permission) BeforeCreate(tx *gorm.DB) error {
|
||||
p.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize fields
|
||||
p.Name = strings.ToLower(strings.TrimSpace(p.Name))
|
||||
p.Description = strings.TrimSpace(p.Description)
|
||||
p.Category = strings.ToLower(strings.TrimSpace(p.Category))
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a permission
|
||||
func (p *Permission) BeforeUpdate(tx *gorm.DB) error {
|
||||
p.BaseModel.BeforeUpdate()
|
||||
|
||||
// Normalize fields if they're being updated
|
||||
if p.Name != "" {
|
||||
p.Name = strings.ToLower(strings.TrimSpace(p.Name))
|
||||
}
|
||||
if p.Description != "" {
|
||||
p.Description = strings.TrimSpace(p.Description)
|
||||
}
|
||||
if p.Category != "" {
|
||||
p.Category = strings.ToLower(strings.TrimSpace(p.Category))
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// BeforeDelete is called before deleting a permission
|
||||
func (p *Permission) BeforeDelete(tx *gorm.DB) error {
|
||||
if p.System {
|
||||
return errors.New("system permissions cannot be deleted")
|
||||
}
|
||||
|
||||
// Check if permission is assigned to any roles
|
||||
var roleCount int64
|
||||
if err := tx.Model(&Role{}).Where("permissions.id = ?", p.ID).Joins("JOIN role_permissions ON roles.id = role_permissions.role_id").Count(&roleCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if roleCount > 0 {
|
||||
return errors.New("cannot delete permission that is assigned to roles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates permission data
|
||||
func (p *Permission) Validate() error {
|
||||
if p.Name == "" {
|
||||
return errors.New("permission name is required")
|
||||
}
|
||||
|
||||
if len(p.Name) < 3 || len(p.Name) > 100 {
|
||||
return errors.New("permission name must be between 3 and 100 characters")
|
||||
}
|
||||
|
||||
if !isValidPermissionName(p.Name) {
|
||||
return errors.New("permission name must follow the format 'resource:action' (e.g., 'user:create')")
|
||||
}
|
||||
|
||||
if p.Category == "" {
|
||||
return errors.New("permission category is required")
|
||||
}
|
||||
|
||||
if len(p.Category) > 50 {
|
||||
return errors.New("permission category must not exceed 50 characters")
|
||||
}
|
||||
|
||||
if len(p.Description) > 500 {
|
||||
return errors.New("permission description must not exceed 500 characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToPermissionInfo converts Permission to PermissionInfo (public information)
|
||||
func (p *Permission) ToPermissionInfo() PermissionInfo {
|
||||
return PermissionInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
Category: p.Category,
|
||||
Active: p.Active,
|
||||
System: p.System,
|
||||
DateCreated: p.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetResource extracts the resource part from a permission name (e.g., "user:create" -> "user")
|
||||
func (p *Permission) GetResource() string {
|
||||
parts := strings.Split(p.Name, ":")
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetAction extracts the action part from a permission name (e.g., "user:create" -> "create")
|
||||
func (p *Permission) GetAction() string {
|
||||
parts := strings.Split(p.Name, ":")
|
||||
if len(parts) > 1 {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isValidPermissionName validates permission name format
|
||||
func isValidPermissionName(name string) bool {
|
||||
// Permission names should follow the format "resource:action"
|
||||
parts := strings.Split(name, ":")
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
resource := parts[0]
|
||||
action := parts[1]
|
||||
|
||||
// Validate resource part
|
||||
if len(resource) < 2 || len(resource) > 50 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate action part
|
||||
if len(action) < 2 || len(action) > 50 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if both parts contain only valid characters
|
||||
return isValidIdentifier(resource) && isValidIdentifier(action)
|
||||
}
|
||||
|
||||
// isValidIdentifier checks if a string is a valid identifier (letters, numbers, underscores)
|
||||
func isValidIdentifier(str string) bool {
|
||||
for _, char := range str {
|
||||
if !((char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') ||
|
||||
char == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Common permission categories
|
||||
const (
|
||||
PermissionCategoryUser = "user"
|
||||
PermissionCategoryRole = "role"
|
||||
PermissionCategorySystem = "system"
|
||||
PermissionCategoryContent = "content"
|
||||
PermissionCategoryReport = "report"
|
||||
)
|
||||
|
||||
// Common permission patterns
|
||||
const (
|
||||
PermissionCreate = "create"
|
||||
PermissionRead = "read"
|
||||
PermissionUpdate = "update"
|
||||
PermissionDelete = "delete"
|
||||
PermissionManage = "manage"
|
||||
PermissionAdmin = "admin"
|
||||
)
|
||||
|
||||
// GetStandardPermissions returns a list of standard permissions for a resource
|
||||
func GetStandardPermissions(resource string) []Permission {
|
||||
return []Permission{
|
||||
{
|
||||
Name: resource + ":" + PermissionCreate,
|
||||
Description: "Create new " + resource + " records",
|
||||
Category: resource,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
Name: resource + ":" + PermissionRead,
|
||||
Description: "Read " + resource + " records",
|
||||
Category: resource,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
Name: resource + ":" + PermissionUpdate,
|
||||
Description: "Update " + resource + " records",
|
||||
Category: resource,
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
Name: resource + ":" + PermissionDelete,
|
||||
Description: "Delete " + resource + " records",
|
||||
Category: resource,
|
||||
Active: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Common system permissions
|
||||
const (
|
||||
ServerView = "server:view"
|
||||
ServerUpdate = "server:update"
|
||||
ServerStart = "server:start"
|
||||
ServerStop = "server:stop"
|
||||
ConfigView = "config:view"
|
||||
ConfigUpdate = "config:update"
|
||||
)
|
||||
|
||||
// AllPermissions returns all available permissions in the system
|
||||
var AllPermissions = []string{
|
||||
"user:create",
|
||||
"user:read",
|
||||
"user:update",
|
||||
"user:delete",
|
||||
"role:create",
|
||||
"role:read",
|
||||
"role:update",
|
||||
"role:delete",
|
||||
"permission:create",
|
||||
"permission:read",
|
||||
"permission:update",
|
||||
"permission:delete",
|
||||
ServerView,
|
||||
ServerUpdate,
|
||||
ServerStart,
|
||||
ServerStop,
|
||||
ConfigView,
|
||||
ConfigUpdate,
|
||||
"audit:read",
|
||||
"system:admin",
|
||||
}
|
||||
182
local/model/role.go
Normal file
182
local/model/role.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Role represents a role in the system
|
||||
type Role struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"unique;not null;type:varchar(100)"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Active bool `json:"active" gorm:"default:true"`
|
||||
System bool `json:"system" gorm:"default:false"` // System roles cannot be deleted
|
||||
Users []User `json:"-" gorm:"many2many:user_roles;"`
|
||||
Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
|
||||
}
|
||||
|
||||
// RoleCreateRequest represents the request to create a new role
|
||||
type RoleCreateRequest struct {
|
||||
Name string `json:"name" validate:"required,min=3,max=100"`
|
||||
Description string `json:"description" validate:"max=500"`
|
||||
PermissionIDs []string `json:"permissionIds"`
|
||||
}
|
||||
|
||||
// RoleUpdateRequest represents the request to update a role
|
||||
type RoleUpdateRequest struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=100"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty,max=500"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
PermissionIDs []string `json:"permissionIds,omitempty"`
|
||||
}
|
||||
|
||||
// RoleInfo represents public role information
|
||||
type RoleInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Active bool `json:"active"`
|
||||
System bool `json:"system"`
|
||||
Permissions []PermissionInfo `json:"permissions"`
|
||||
UserCount int64 `json:"userCount"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a role
|
||||
func (r *Role) BeforeCreate(tx *gorm.DB) error {
|
||||
r.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize name
|
||||
r.Name = strings.ToLower(strings.TrimSpace(r.Name))
|
||||
r.Description = strings.TrimSpace(r.Description)
|
||||
|
||||
return r.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a role
|
||||
func (r *Role) BeforeUpdate(tx *gorm.DB) error {
|
||||
r.BaseModel.BeforeUpdate()
|
||||
|
||||
// Normalize fields if they're being updated
|
||||
if r.Name != "" {
|
||||
r.Name = strings.ToLower(strings.TrimSpace(r.Name))
|
||||
}
|
||||
if r.Description != "" {
|
||||
r.Description = strings.TrimSpace(r.Description)
|
||||
}
|
||||
|
||||
return r.Validate()
|
||||
}
|
||||
|
||||
// BeforeDelete is called before deleting a role
|
||||
func (r *Role) BeforeDelete(tx *gorm.DB) error {
|
||||
if r.System {
|
||||
return errors.New("system roles cannot be deleted")
|
||||
}
|
||||
|
||||
// Check if role is assigned to any users
|
||||
var userCount int64
|
||||
if err := tx.Model(&User{}).Where("roles.id = ?", r.ID).Joins("JOIN user_roles ON users.id = user_roles.user_id").Count(&userCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userCount > 0 {
|
||||
return errors.New("cannot delete role that is assigned to users")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates role data
|
||||
func (r *Role) Validate() error {
|
||||
if r.Name == "" {
|
||||
return errors.New("role name is required")
|
||||
}
|
||||
|
||||
if len(r.Name) < 3 || len(r.Name) > 100 {
|
||||
return errors.New("role name must be between 3 and 100 characters")
|
||||
}
|
||||
|
||||
if !isValidRoleName(r.Name) {
|
||||
return errors.New("role name can only contain letters, numbers, underscores, and hyphens")
|
||||
}
|
||||
|
||||
if len(r.Description) > 500 {
|
||||
return errors.New("role description must not exceed 500 characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToRoleInfo converts Role to RoleInfo (public information)
|
||||
func (r *Role) ToRoleInfo() RoleInfo {
|
||||
roleInfo := RoleInfo{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
Description: r.Description,
|
||||
Active: r.Active,
|
||||
System: r.System,
|
||||
Permissions: make([]PermissionInfo, len(r.Permissions)),
|
||||
DateCreated: r.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
// Convert permissions
|
||||
for i, permission := range r.Permissions {
|
||||
roleInfo.Permissions[i] = permission.ToPermissionInfo()
|
||||
}
|
||||
|
||||
return roleInfo
|
||||
}
|
||||
|
||||
// HasPermission checks if the role has a specific permission
|
||||
func (r *Role) HasPermission(permissionName string) bool {
|
||||
for _, permission := range r.Permissions {
|
||||
if permission.Name == permissionName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddPermission adds a permission to the role
|
||||
func (r *Role) AddPermission(permission Permission) {
|
||||
if !r.HasPermission(permission.Name) {
|
||||
r.Permissions = append(r.Permissions, permission)
|
||||
}
|
||||
}
|
||||
|
||||
// RemovePermission removes a permission from the role
|
||||
func (r *Role) RemovePermission(permissionName string) {
|
||||
for i, permission := range r.Permissions {
|
||||
if permission.Name == permissionName {
|
||||
r.Permissions = append(r.Permissions[:i], r.Permissions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetPermissionNames returns a slice of permission names
|
||||
func (r *Role) GetPermissionNames() []string {
|
||||
names := make([]string, len(r.Permissions))
|
||||
for i, permission := range r.Permissions {
|
||||
names[i] = permission.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// isValidRoleName validates role name format
|
||||
func isValidRoleName(name string) bool {
|
||||
// Allow letters, numbers, underscores, and hyphens
|
||||
for _, char := range name {
|
||||
if !((char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') ||
|
||||
char == '_' || char == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
367
local/model/security_event.go
Normal file
367
local/model/security_event.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SecurityEvent represents a security event in the system
|
||||
type SecurityEvent struct {
|
||||
BaseModel
|
||||
EventType string `json:"eventType" gorm:"not null;type:varchar(100);index"`
|
||||
Severity string `json:"severity" gorm:"not null;type:varchar(20);index"`
|
||||
UserID string `json:"userId" gorm:"type:varchar(36);index"`
|
||||
IPAddress string `json:"ipAddress" gorm:"type:varchar(45);index"`
|
||||
UserAgent string `json:"userAgent" gorm:"type:text"`
|
||||
Resource string `json:"resource" gorm:"type:varchar(100)"`
|
||||
Action string `json:"action" gorm:"type:varchar(100)"`
|
||||
Success bool `json:"success" gorm:"index"`
|
||||
Blocked bool `json:"blocked" gorm:"default:false;index"`
|
||||
Message string `json:"message" gorm:"type:text"`
|
||||
Details map[string]interface{} `json:"details" gorm:"type:text"`
|
||||
SessionID string `json:"sessionId,omitempty" gorm:"type:varchar(255)"`
|
||||
RequestID string `json:"requestId,omitempty" gorm:"type:varchar(255)"`
|
||||
CountryCode string `json:"countryCode,omitempty" gorm:"type:varchar(2)"`
|
||||
City string `json:"city,omitempty" gorm:"type:varchar(100)"`
|
||||
Resolved bool `json:"resolved" gorm:"default:false;index"`
|
||||
ResolvedBy string `json:"resolvedBy,omitempty" gorm:"type:varchar(36)"`
|
||||
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
|
||||
Notes string `json:"notes,omitempty" gorm:"type:text"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Resolver *User `json:"resolver,omitempty" gorm:"foreignKey:ResolvedBy"`
|
||||
}
|
||||
|
||||
// SecurityEventCreateRequest represents the request to create a new security event
|
||||
type SecurityEventCreateRequest struct {
|
||||
EventType string `json:"eventType" validate:"required,max=100"`
|
||||
Severity string `json:"severity" validate:"required,oneof=low medium high critical"`
|
||||
UserID string `json:"userId"`
|
||||
IPAddress string `json:"ipAddress" validate:"max=45"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Resource string `json:"resource" validate:"max=100"`
|
||||
Action string `json:"action" validate:"max=100"`
|
||||
Success bool `json:"success"`
|
||||
Blocked bool `json:"blocked"`
|
||||
Message string `json:"message" validate:"required"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
SessionID string `json:"sessionId"`
|
||||
RequestID string `json:"requestId"`
|
||||
CountryCode string `json:"countryCode" validate:"max=2"`
|
||||
City string `json:"city" validate:"max=100"`
|
||||
}
|
||||
|
||||
// SecurityEventUpdateRequest represents the request to update a security event
|
||||
type SecurityEventUpdateRequest struct {
|
||||
Resolved *bool `json:"resolved,omitempty"`
|
||||
ResolvedBy *string `json:"resolvedBy,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
// SecurityEventInfo represents public security event information
|
||||
type SecurityEventInfo struct {
|
||||
ID string `json:"id"`
|
||||
EventType string `json:"eventType"`
|
||||
Severity string `json:"severity"`
|
||||
UserID string `json:"userId"`
|
||||
UserEmail string `json:"userEmail,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
IPAddress string `json:"ipAddress"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
Success bool `json:"success"`
|
||||
Blocked bool `json:"blocked"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
CountryCode string `json:"countryCode,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Resolved bool `json:"resolved"`
|
||||
ResolvedBy string `json:"resolvedBy,omitempty"`
|
||||
ResolverEmail string `json:"resolverEmail,omitempty"`
|
||||
ResolverName string `json:"resolverName,omitempty"`
|
||||
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a security event
|
||||
func (se *SecurityEvent) BeforeCreate(tx *gorm.DB) error {
|
||||
se.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize fields
|
||||
se.EventType = strings.ToLower(strings.TrimSpace(se.EventType))
|
||||
se.Severity = strings.ToLower(strings.TrimSpace(se.Severity))
|
||||
se.IPAddress = strings.TrimSpace(se.IPAddress)
|
||||
se.UserAgent = strings.TrimSpace(se.UserAgent)
|
||||
se.Resource = strings.ToLower(strings.TrimSpace(se.Resource))
|
||||
se.Action = strings.ToLower(strings.TrimSpace(se.Action))
|
||||
se.Message = strings.TrimSpace(se.Message)
|
||||
|
||||
return se.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a security event
|
||||
func (se *SecurityEvent) BeforeUpdate(tx *gorm.DB) error {
|
||||
se.BaseModel.BeforeUpdate()
|
||||
|
||||
// If resolving the event, set resolved timestamp
|
||||
if se.Resolved && se.ResolvedAt == nil {
|
||||
now := time.Now()
|
||||
se.ResolvedAt = &now
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates security event data
|
||||
func (se *SecurityEvent) Validate() error {
|
||||
validSeverities := []string{"low", "medium", "high", "critical"}
|
||||
isValidSeverity := false
|
||||
for _, severity := range validSeverities {
|
||||
if se.Severity == severity {
|
||||
isValidSeverity = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValidSeverity {
|
||||
return gorm.ErrInvalidValue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDetails sets the details field from a map
|
||||
func (se *SecurityEvent) SetDetails(details map[string]interface{}) error {
|
||||
se.Details = details
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDetails returns the details as a map
|
||||
func (se *SecurityEvent) GetDetails() map[string]interface{} {
|
||||
if se.Details == nil {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
return se.Details
|
||||
}
|
||||
|
||||
// SetDetailsFromJSON sets the details field from a JSON string
|
||||
func (se *SecurityEvent) SetDetailsFromJSON(jsonStr string) error {
|
||||
if jsonStr == "" {
|
||||
se.Details = make(map[string]interface{})
|
||||
return nil
|
||||
}
|
||||
|
||||
var details map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &details); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
se.Details = details
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDetailsAsJSON returns the details as a JSON string
|
||||
func (se *SecurityEvent) GetDetailsAsJSON() (string, error) {
|
||||
if se.Details == nil || len(se.Details) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(se.Details)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// ToSecurityEventInfo converts SecurityEvent to SecurityEventInfo (public information)
|
||||
func (se *SecurityEvent) ToSecurityEventInfo() SecurityEventInfo {
|
||||
info := SecurityEventInfo{
|
||||
ID: se.ID,
|
||||
EventType: se.EventType,
|
||||
Severity: se.Severity,
|
||||
UserID: se.UserID,
|
||||
IPAddress: se.IPAddress,
|
||||
UserAgent: se.UserAgent,
|
||||
Resource: se.Resource,
|
||||
Action: se.Action,
|
||||
Success: se.Success,
|
||||
Blocked: se.Blocked,
|
||||
Message: se.Message,
|
||||
Details: se.GetDetails(),
|
||||
SessionID: se.SessionID,
|
||||
RequestID: se.RequestID,
|
||||
CountryCode: se.CountryCode,
|
||||
City: se.City,
|
||||
Resolved: se.Resolved,
|
||||
ResolvedBy: se.ResolvedBy,
|
||||
ResolvedAt: se.ResolvedAt,
|
||||
Notes: se.Notes,
|
||||
DateCreated: se.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
// Include user information if available
|
||||
if se.User != nil {
|
||||
info.UserEmail = se.User.Email
|
||||
info.UserName = se.User.Name
|
||||
}
|
||||
|
||||
// Include resolver information if available
|
||||
if se.Resolver != nil {
|
||||
info.ResolverEmail = se.Resolver.Email
|
||||
info.ResolverName = se.Resolver.Name
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// AddDetail adds a single detail to the details map
|
||||
func (se *SecurityEvent) AddDetail(key string, value interface{}) {
|
||||
if se.Details == nil {
|
||||
se.Details = make(map[string]interface{})
|
||||
}
|
||||
se.Details[key] = value
|
||||
}
|
||||
|
||||
// GetDetail gets a single detail from the details map
|
||||
func (se *SecurityEvent) GetDetail(key string) (interface{}, bool) {
|
||||
if se.Details == nil {
|
||||
return nil, false
|
||||
}
|
||||
value, exists := se.Details[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// Resolve resolves the security event
|
||||
func (se *SecurityEvent) Resolve(resolverID, notes string) {
|
||||
se.Resolved = true
|
||||
se.ResolvedBy = resolverID
|
||||
se.Notes = notes
|
||||
now := time.Now()
|
||||
se.ResolvedAt = &now
|
||||
}
|
||||
|
||||
// Unresolve unresolves the security event
|
||||
func (se *SecurityEvent) Unresolve() {
|
||||
se.Resolved = false
|
||||
se.ResolvedBy = ""
|
||||
se.ResolvedAt = nil
|
||||
}
|
||||
|
||||
// IsResolved returns whether the security event is resolved
|
||||
func (se *SecurityEvent) IsResolved() bool {
|
||||
return se.Resolved
|
||||
}
|
||||
|
||||
// IsCritical returns whether the security event is critical
|
||||
func (se *SecurityEvent) IsCritical() bool {
|
||||
return se.Severity == SecuritySeverityCritical
|
||||
}
|
||||
|
||||
// IsHigh returns whether the security event is high severity
|
||||
func (se *SecurityEvent) IsHigh() bool {
|
||||
return se.Severity == SecuritySeverityHigh
|
||||
}
|
||||
|
||||
// IsBlocked returns whether the security event was blocked
|
||||
func (se *SecurityEvent) IsBlocked() bool {
|
||||
return se.Blocked
|
||||
}
|
||||
|
||||
// Security event types
|
||||
const (
|
||||
SecurityEventLoginAttempt = "login_attempt"
|
||||
SecurityEventLoginFailure = "login_failure"
|
||||
SecurityEventLoginSuccess = "login_success"
|
||||
SecurityEventBruteForce = "brute_force"
|
||||
SecurityEventAccountLockout = "account_lockout"
|
||||
SecurityEventUnauthorizedAccess = "unauthorized_access"
|
||||
SecurityEventPrivilegeEscalation = "privilege_escalation"
|
||||
SecurityEventSuspiciousActivity = "suspicious_activity"
|
||||
SecurityEventRateLimitExceeded = "rate_limit_exceeded"
|
||||
SecurityEventInvalidToken = "invalid_token"
|
||||
SecurityEventTokenExpired = "token_expired"
|
||||
SecurityEventPasswordChange = "password_change"
|
||||
SecurityEventEmailVerification = "email_verification"
|
||||
SecurityEventTwoFactorAuth = "two_factor_auth"
|
||||
SecurityEventDataExfiltration = "data_exfiltration"
|
||||
SecurityEventMaliciousRequest = "malicious_request"
|
||||
SecurityEventSystemAccess = "system_access"
|
||||
SecurityEventConfigChange = "config_change"
|
||||
SecurityEventFileAccess = "file_access"
|
||||
SecurityEventDatabaseAccess = "database_access"
|
||||
)
|
||||
|
||||
// Security severity levels
|
||||
const (
|
||||
SecuritySeverityLow = "low"
|
||||
SecuritySeverityMedium = "medium"
|
||||
SecuritySeverityHigh = "high"
|
||||
SecuritySeverityCritical = "critical"
|
||||
)
|
||||
|
||||
// CreateSecurityEvent creates a new security event
|
||||
func CreateSecurityEvent(eventType, severity, message string) *SecurityEvent {
|
||||
event := &SecurityEvent{
|
||||
EventType: eventType,
|
||||
Severity: severity,
|
||||
Message: message,
|
||||
Details: make(map[string]interface{}),
|
||||
}
|
||||
event.Init()
|
||||
return event
|
||||
}
|
||||
|
||||
// CreateSecurityEventWithUser creates a new security event with user information
|
||||
func CreateSecurityEventWithUser(eventType, severity, message, userID string) *SecurityEvent {
|
||||
event := CreateSecurityEvent(eventType, severity, message)
|
||||
event.UserID = userID
|
||||
return event
|
||||
}
|
||||
|
||||
// CreateSecurityEventWithDetails creates a new security event with details
|
||||
func CreateSecurityEventWithDetails(eventType, severity, message string, details map[string]interface{}) *SecurityEvent {
|
||||
event := CreateSecurityEvent(eventType, severity, message)
|
||||
event.Details = details
|
||||
return event
|
||||
}
|
||||
|
||||
// SetRequestInfo sets request-related information
|
||||
func (se *SecurityEvent) SetRequestInfo(ipAddress, userAgent, sessionID, requestID string) {
|
||||
se.IPAddress = ipAddress
|
||||
se.UserAgent = userAgent
|
||||
se.SessionID = sessionID
|
||||
se.RequestID = requestID
|
||||
}
|
||||
|
||||
// SetLocationInfo sets location-related information
|
||||
func (se *SecurityEvent) SetLocationInfo(countryCode, city string) {
|
||||
se.CountryCode = countryCode
|
||||
se.City = city
|
||||
}
|
||||
|
||||
// SetResourceAction sets resource and action information
|
||||
func (se *SecurityEvent) SetResourceAction(resource, action string) {
|
||||
se.Resource = resource
|
||||
se.Action = action
|
||||
}
|
||||
|
||||
// MarkAsBlocked marks the security event as blocked
|
||||
func (se *SecurityEvent) MarkAsBlocked() {
|
||||
se.Blocked = true
|
||||
}
|
||||
|
||||
// MarkAsSuccess marks the security event as successful
|
||||
func (se *SecurityEvent) MarkAsSuccess() {
|
||||
se.Success = true
|
||||
}
|
||||
|
||||
// MarkAsFailure marks the security event as failed
|
||||
func (se *SecurityEvent) MarkAsFailure() {
|
||||
se.Success = false
|
||||
}
|
||||
280
local/model/system_config.go
Normal file
280
local/model/system_config.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SystemConfig represents a system configuration setting
|
||||
type SystemConfig struct {
|
||||
BaseModel
|
||||
Key string `json:"key" gorm:"unique;not null;type:varchar(255)"`
|
||||
Value string `json:"value" gorm:"type:text"`
|
||||
DefaultValue string `json:"defaultValue" gorm:"type:text"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Category string `json:"category" gorm:"type:varchar(100)"`
|
||||
DataType string `json:"dataType" gorm:"type:varchar(50)"` // string, integer, boolean, json
|
||||
IsEditable bool `json:"isEditable" gorm:"default:true"`
|
||||
IsSecret bool `json:"isSecret" gorm:"default:false"` // For sensitive values
|
||||
DateModified string `json:"dateModified" gorm:"type:varchar(50)"`
|
||||
}
|
||||
|
||||
// SystemConfigCreateRequest represents the request to create a new system config
|
||||
type SystemConfigCreateRequest struct {
|
||||
Key string `json:"key" validate:"required,min=3,max=255"`
|
||||
Value string `json:"value"`
|
||||
DefaultValue string `json:"defaultValue"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Category string `json:"category" validate:"required,max=100"`
|
||||
DataType string `json:"dataType" validate:"required,oneof=string integer boolean json"`
|
||||
IsEditable bool `json:"isEditable"`
|
||||
IsSecret bool `json:"isSecret"`
|
||||
}
|
||||
|
||||
// SystemConfigUpdateRequest represents the request to update a system config
|
||||
type SystemConfigUpdateRequest struct {
|
||||
Value *string `json:"value,omitempty"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
||||
Category *string `json:"category,omitempty" validate:"omitempty,max=100"`
|
||||
DataType *string `json:"dataType,omitempty" validate:"omitempty,oneof=string integer boolean json"`
|
||||
IsEditable *bool `json:"isEditable,omitempty"`
|
||||
IsSecret *bool `json:"isSecret,omitempty"`
|
||||
}
|
||||
|
||||
// SystemConfigInfo represents public system config information
|
||||
type SystemConfigInfo struct {
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value,omitempty"` // Omitted if secret
|
||||
DefaultValue string `json:"defaultValue,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
DataType string `json:"dataType"`
|
||||
IsEditable bool `json:"isEditable"`
|
||||
IsSecret bool `json:"isSecret"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
DateModified string `json:"dateModified"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a system config
|
||||
func (sc *SystemConfig) BeforeCreate(tx *gorm.DB) error {
|
||||
sc.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize key and category
|
||||
sc.Key = strings.ToLower(strings.TrimSpace(sc.Key))
|
||||
sc.Category = strings.ToLower(strings.TrimSpace(sc.Category))
|
||||
sc.Description = strings.TrimSpace(sc.Description)
|
||||
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
return sc.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a system config
|
||||
func (sc *SystemConfig) BeforeUpdate(tx *gorm.DB) error {
|
||||
sc.BaseModel.BeforeUpdate()
|
||||
|
||||
// Update modification timestamp
|
||||
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
// Normalize fields if they're being updated
|
||||
if sc.Key != "" {
|
||||
sc.Key = strings.ToLower(strings.TrimSpace(sc.Key))
|
||||
}
|
||||
if sc.Category != "" {
|
||||
sc.Category = strings.ToLower(strings.TrimSpace(sc.Category))
|
||||
}
|
||||
if sc.Description != "" {
|
||||
sc.Description = strings.TrimSpace(sc.Description)
|
||||
}
|
||||
|
||||
return sc.Validate()
|
||||
}
|
||||
|
||||
// Validate validates system config data
|
||||
func (sc *SystemConfig) Validate() error {
|
||||
if sc.Key == "" {
|
||||
return errors.New("configuration key is required")
|
||||
}
|
||||
|
||||
if len(sc.Key) < 3 || len(sc.Key) > 255 {
|
||||
return errors.New("configuration key must be between 3 and 255 characters")
|
||||
}
|
||||
|
||||
if !isValidConfigKey(sc.Key) {
|
||||
return errors.New("configuration key can only contain letters, numbers, dots, underscores, and hyphens")
|
||||
}
|
||||
|
||||
if sc.Category == "" {
|
||||
return errors.New("configuration category is required")
|
||||
}
|
||||
|
||||
if len(sc.Category) > 100 {
|
||||
return errors.New("configuration category must not exceed 100 characters")
|
||||
}
|
||||
|
||||
if sc.DataType == "" {
|
||||
return errors.New("configuration data type is required")
|
||||
}
|
||||
|
||||
if !isValidDataType(sc.DataType) {
|
||||
return errors.New("invalid data type, must be one of: string, integer, boolean, json")
|
||||
}
|
||||
|
||||
if len(sc.Description) > 1000 {
|
||||
return errors.New("configuration description must not exceed 1000 characters")
|
||||
}
|
||||
|
||||
// Validate value according to data type
|
||||
if sc.Value != "" {
|
||||
if err := sc.ValidateValue(sc.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateValue validates the configuration value according to its data type
|
||||
func (sc *SystemConfig) ValidateValue(value string) error {
|
||||
switch sc.DataType {
|
||||
case "integer":
|
||||
if _, err := strconv.Atoi(value); err != nil {
|
||||
return errors.New("value must be a valid integer")
|
||||
}
|
||||
case "boolean":
|
||||
if _, err := strconv.ParseBool(value); err != nil {
|
||||
return errors.New("value must be a valid boolean (true/false)")
|
||||
}
|
||||
case "json":
|
||||
// Basic JSON validation - check if it starts with { or [
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
|
||||
return errors.New("value must be valid JSON")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToSystemConfigInfo converts SystemConfig to SystemConfigInfo (public information)
|
||||
func (sc *SystemConfig) ToSystemConfigInfo() SystemConfigInfo {
|
||||
info := SystemConfigInfo{
|
||||
ID: sc.ID,
|
||||
Key: sc.Key,
|
||||
DefaultValue: sc.DefaultValue,
|
||||
Description: sc.Description,
|
||||
Category: sc.Category,
|
||||
DataType: sc.DataType,
|
||||
IsEditable: sc.IsEditable,
|
||||
IsSecret: sc.IsSecret,
|
||||
DateCreated: sc.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
DateModified: sc.DateModified,
|
||||
}
|
||||
|
||||
// Only include value if it's not a secret
|
||||
if !sc.IsSecret {
|
||||
info.Value = sc.Value
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// GetStringValue returns the configuration value as a string
|
||||
func (sc *SystemConfig) GetStringValue() string {
|
||||
if sc.Value != "" {
|
||||
return sc.Value
|
||||
}
|
||||
return sc.DefaultValue
|
||||
}
|
||||
|
||||
// GetIntValue returns the configuration value as an integer
|
||||
func (sc *SystemConfig) GetIntValue() (int, error) {
|
||||
value := sc.GetStringValue()
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
// GetBoolValue returns the configuration value as a boolean
|
||||
func (sc *SystemConfig) GetBoolValue() (bool, error) {
|
||||
value := sc.GetStringValue()
|
||||
return strconv.ParseBool(value)
|
||||
}
|
||||
|
||||
// GetFloatValue returns the configuration value as a float64
|
||||
func (sc *SystemConfig) GetFloatValue() (float64, error) {
|
||||
value := sc.GetStringValue()
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
|
||||
// SetValue sets the configuration value with type validation
|
||||
func (sc *SystemConfig) SetValue(value string) error {
|
||||
if err := sc.ValidateValue(value); err != nil {
|
||||
return err
|
||||
}
|
||||
sc.Value = value
|
||||
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetToDefault resets the configuration value to its default
|
||||
func (sc *SystemConfig) ResetToDefault() {
|
||||
sc.Value = sc.DefaultValue
|
||||
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// isValidConfigKey validates configuration key format
|
||||
func isValidConfigKey(key string) bool {
|
||||
// Allow letters, numbers, dots, underscores, and hyphens
|
||||
for _, char := range key {
|
||||
if !((char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') ||
|
||||
char == '.' || char == '_' || char == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidDataType validates the data type
|
||||
func isValidDataType(dataType string) bool {
|
||||
validTypes := []string{"string", "integer", "boolean", "json"}
|
||||
for _, validType := range validTypes {
|
||||
if dataType == validType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Common system configuration categories
|
||||
const (
|
||||
ConfigCategoryGeneral = "general"
|
||||
ConfigCategorySecurity = "security"
|
||||
ConfigCategoryEmail = "email"
|
||||
ConfigCategoryAPI = "api"
|
||||
ConfigCategoryLogging = "logging"
|
||||
ConfigCategoryStorage = "storage"
|
||||
ConfigCategoryCache = "cache"
|
||||
)
|
||||
|
||||
// Common system configuration keys
|
||||
const (
|
||||
ConfigKeyAppName = "app.name"
|
||||
ConfigKeyAppVersion = "app.version"
|
||||
ConfigKeyAppDescription = "app.description"
|
||||
ConfigKeyJWTExpiryHours = "security.jwt_expiry_hours"
|
||||
ConfigKeyPasswordMinLength = "security.password_min_length"
|
||||
ConfigKeyMaxLoginAttempts = "security.max_login_attempts"
|
||||
ConfigKeyLockoutDurationMinutes = "security.lockout_duration_minutes"
|
||||
ConfigKeySessionTimeoutMinutes = "security.session_timeout_minutes"
|
||||
ConfigKeyRateLimitRequests = "security.rate_limit_requests"
|
||||
ConfigKeyRateLimitWindow = "security.rate_limit_window_minutes"
|
||||
ConfigKeyLogLevel = "logging.level"
|
||||
ConfigKeyLogRetentionDays = "logging.retention_days"
|
||||
ConfigKeyMaxFileUploadSize = "storage.max_file_upload_size_mb"
|
||||
ConfigKeyCacheEnabled = "cache.enabled"
|
||||
ConfigKeyCacheTTLMinutes = "cache.ttl_minutes"
|
||||
)
|
||||
318
local/model/user.go
Normal file
318
local/model/user.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"omega-server/local/utl/password"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User represents a user in the system
|
||||
type User struct {
|
||||
BaseModel
|
||||
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
|
||||
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"`
|
||||
Name string `json:"name" gorm:"not null;type:varchar(255)"`
|
||||
PasswordHash string `json:"-" gorm:"not null;type:text"`
|
||||
Active bool `json:"active" gorm:"default:true"`
|
||||
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
|
||||
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
|
||||
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
|
||||
PasswordResetExpires *time.Time `json:"-"`
|
||||
LastLogin *time.Time `json:"lastLogin"`
|
||||
LoginAttempts int `json:"-" gorm:"default:0"`
|
||||
LockedUntil *time.Time `json:"-"`
|
||||
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
|
||||
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
|
||||
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
|
||||
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// UserCreateRequest represents the request to create a new user
|
||||
type UserCreateRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
||||
Name string `json:"name" validate:"required,min=2,max=100"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
RoleIDs []string `json:"roleIds"`
|
||||
}
|
||||
|
||||
// UserUpdateRequest represents the request to update a user
|
||||
type UserUpdateRequest struct {
|
||||
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
||||
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
RoleIDs []string `json:"roleIds,omitempty"`
|
||||
}
|
||||
|
||||
// UserLoginRequest represents a login request
|
||||
type UserLoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
// UserLoginResponse represents a login response
|
||||
type UserLoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
User UserInfo `json:"user"`
|
||||
}
|
||||
|
||||
// UserInfo represents public user information
|
||||
type UserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
LastLogin *time.Time `json:"lastLogin"`
|
||||
Roles []RoleInfo `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest represents a password change request
|
||||
type ChangePasswordRequest struct {
|
||||
CurrentPassword string `json:"currentPassword" validate:"required"`
|
||||
NewPassword string `json:"newPassword" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// ResetPasswordRequest represents a password reset request
|
||||
type ResetPasswordRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
// ResetPasswordConfirmRequest represents a password reset confirmation
|
||||
type ResetPasswordConfirmRequest struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
NewPassword string `json:"newPassword" validate:"required,min=8"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a user
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
u.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize email and username
|
||||
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
||||
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
||||
u.Name = strings.TrimSpace(u.Name)
|
||||
|
||||
return u.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a user
|
||||
func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
u.BaseModel.BeforeUpdate()
|
||||
|
||||
// Normalize fields if they're being updated
|
||||
if u.Email != "" {
|
||||
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
||||
}
|
||||
if u.Username != "" {
|
||||
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
||||
}
|
||||
if u.Name != "" {
|
||||
u.Name = strings.TrimSpace(u.Name)
|
||||
}
|
||||
|
||||
return u.Validate()
|
||||
}
|
||||
|
||||
// Validate validates user data
|
||||
func (u *User) Validate() error {
|
||||
if u.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
|
||||
if !isValidEmail(u.Email) {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
|
||||
if u.Username == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
|
||||
if len(u.Username) < 3 || len(u.Username) > 50 {
|
||||
return errors.New("username must be between 3 and 50 characters")
|
||||
}
|
||||
|
||||
if !isValidUsername(u.Username) {
|
||||
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
|
||||
}
|
||||
|
||||
if u.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
if len(u.Name) < 2 || len(u.Name) > 100 {
|
||||
return errors.New("name must be between 2 and 100 characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPassword sets the user's password hash
|
||||
func (u *User) SetPassword(plainPassword string) error {
|
||||
if err := validatePassword(plainPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := password.HashPassword(plainPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.PasswordHash = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword verifies the user's password
|
||||
func (u *User) CheckPassword(plainPassword string) bool {
|
||||
return password.CheckPasswordHash(plainPassword, u.PasswordHash)
|
||||
}
|
||||
|
||||
// VerifyPassword verifies the user's password (alias for CheckPassword)
|
||||
func (u *User) VerifyPassword(plainPassword string) bool {
|
||||
return u.CheckPassword(plainPassword)
|
||||
}
|
||||
|
||||
// IsLocked checks if the user account is locked
|
||||
func (u *User) IsLocked() bool {
|
||||
if u.LockedUntil == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().Before(*u.LockedUntil)
|
||||
}
|
||||
|
||||
// Lock locks the user account for the specified duration
|
||||
func (u *User) Lock(duration time.Duration) {
|
||||
lockUntil := time.Now().Add(duration)
|
||||
u.LockedUntil = &lockUntil
|
||||
}
|
||||
|
||||
// Unlock unlocks the user account
|
||||
func (u *User) Unlock() {
|
||||
u.LockedUntil = nil
|
||||
u.LoginAttempts = 0
|
||||
}
|
||||
|
||||
// IncrementLoginAttempts increments the login attempt counter
|
||||
func (u *User) IncrementLoginAttempts() {
|
||||
u.LoginAttempts++
|
||||
}
|
||||
|
||||
// ResetLoginAttempts resets the login attempt counter
|
||||
func (u *User) ResetLoginAttempts() {
|
||||
u.LoginAttempts = 0
|
||||
}
|
||||
|
||||
// UpdateLastLogin updates the last login timestamp
|
||||
func (u *User) UpdateLastLogin() {
|
||||
now := time.Now()
|
||||
u.LastLogin = &now
|
||||
}
|
||||
|
||||
// ToUserInfo converts User to UserInfo (public information)
|
||||
func (u *User) ToUserInfo() UserInfo {
|
||||
userInfo := UserInfo{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Active: u.Active,
|
||||
EmailVerified: u.EmailVerified,
|
||||
LastLogin: u.LastLogin,
|
||||
DateCreated: u.DateCreated,
|
||||
Roles: make([]RoleInfo, len(u.Roles)),
|
||||
Permissions: []string{},
|
||||
}
|
||||
|
||||
// Convert roles and collect permissions
|
||||
permissionSet := make(map[string]bool)
|
||||
for i, role := range u.Roles {
|
||||
userInfo.Roles[i] = role.ToRoleInfo()
|
||||
for _, permission := range role.Permissions {
|
||||
permissionSet[permission.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert permission set to slice
|
||||
for permission := range permissionSet {
|
||||
userInfo.Permissions = append(userInfo.Permissions, permission)
|
||||
}
|
||||
|
||||
return userInfo
|
||||
}
|
||||
|
||||
// HasRole checks if the user has a specific role
|
||||
func (u *User) HasRole(roleName string) bool {
|
||||
for _, role := range u.Roles {
|
||||
if role.Name == roleName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPermission checks if the user has a specific permission
|
||||
func (u *User) HasPermission(permissionName string) bool {
|
||||
for _, role := range u.Roles {
|
||||
for _, permission := range role.Permissions {
|
||||
if permission.Name == permissionName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidEmail validates email format
|
||||
func isValidEmail(email string) bool {
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
return emailRegex.MatchString(email)
|
||||
}
|
||||
|
||||
// isValidUsername validates username format
|
||||
func isValidUsername(username string) bool {
|
||||
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
||||
return usernameRegex.MatchString(username)
|
||||
}
|
||||
|
||||
// validatePassword validates password strength
|
||||
func validatePassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return errors.New("password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
if len(password) > 128 {
|
||||
return errors.New("password must not exceed 128 characters")
|
||||
}
|
||||
|
||||
// Check for at least one lowercase letter
|
||||
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
|
||||
return errors.New("password must contain at least one lowercase letter")
|
||||
}
|
||||
|
||||
// Check for at least one uppercase letter
|
||||
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
|
||||
return errors.New("password must contain at least one uppercase letter")
|
||||
}
|
||||
|
||||
// Check for at least one digit
|
||||
if matched, _ := regexp.MatchString(`\d`, password); !matched {
|
||||
return errors.New("password must contain at least one digit")
|
||||
}
|
||||
|
||||
// Check for at least one special character
|
||||
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
|
||||
return errors.New("password must contain at least one special character")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
121
local/repository/base.go
Normal file
121
local/repository/base.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BaseRepository provides generic CRUD operations for any model
|
||||
type BaseRepository[T any, F any] struct {
|
||||
db *gorm.DB
|
||||
modelType T
|
||||
}
|
||||
|
||||
// NewBaseRepository creates a new base repository for the given model type
|
||||
func NewBaseRepository[T any, F any](db *gorm.DB, model T) *BaseRepository[T, F] {
|
||||
return &BaseRepository[T, F]{
|
||||
db: db,
|
||||
modelType: model,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll retrieves all records based on the filter
|
||||
func (r *BaseRepository[T, F]) GetAll(ctx context.Context, filter *F) (*[]T, error) {
|
||||
result := new([]T)
|
||||
query := r.db.WithContext(ctx).Model(&r.modelType)
|
||||
|
||||
// Apply filter conditions if filter implements Filterable
|
||||
if filterable, ok := any(filter).(Filterable); ok {
|
||||
query = filterable.ApplyFilter(query)
|
||||
}
|
||||
|
||||
// Apply pagination if filter implements Pageable
|
||||
if pageable, ok := any(filter).(Pageable); ok {
|
||||
offset, limit := pageable.Pagination()
|
||||
query = query.Offset(offset).Limit(limit)
|
||||
}
|
||||
|
||||
// Apply sorting if filter implements Sortable
|
||||
if sortable, ok := any(filter).(Sortable); ok {
|
||||
field, desc := sortable.GetSorting()
|
||||
if desc {
|
||||
query = query.Order(field + " DESC")
|
||||
} else {
|
||||
query = query.Order(field)
|
||||
}
|
||||
}
|
||||
|
||||
if err := query.Find(result).Error; err != nil {
|
||||
return nil, fmt.Errorf("error getting records: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a single record by ID
|
||||
func (r *BaseRepository[T, F]) GetByID(ctx context.Context, id interface{}) (*T, error) {
|
||||
result := new(T)
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(result).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("error getting record by ID: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Insert creates a new record
|
||||
func (r *BaseRepository[T, F]) Insert(ctx context.Context, model *T) error {
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
return fmt.Errorf("error creating record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update modifies an existing record
|
||||
func (r *BaseRepository[T, F]) Update(ctx context.Context, model *T) error {
|
||||
if err := r.db.WithContext(ctx).Save(model).Error; err != nil {
|
||||
return fmt.Errorf("error updating record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a record by ID
|
||||
func (r *BaseRepository[T, F]) Delete(ctx context.Context, id interface{}) error {
|
||||
if err := r.db.WithContext(ctx).Delete(new(T), id).Error; err != nil {
|
||||
return fmt.Errorf("error deleting record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns the total number of records matching the filter
|
||||
func (r *BaseRepository[T, F]) Count(ctx context.Context, filter *F) (int64, error) {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&r.modelType)
|
||||
|
||||
if filterable, ok := any(filter).(Filterable); ok {
|
||||
query = filterable.ApplyFilter(query)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return 0, fmt.Errorf("error counting records: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Interfaces for filter capabilities
|
||||
|
||||
type Filterable interface {
|
||||
ApplyFilter(*gorm.DB) *gorm.DB
|
||||
}
|
||||
|
||||
type Pageable interface {
|
||||
Pagination() (offset, limit int)
|
||||
}
|
||||
|
||||
type Sortable interface {
|
||||
GetSorting() (field string, desc bool)
|
||||
}
|
||||
171
local/repository/membership.go
Normal file
171
local/repository/membership.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"omega-server/local/model"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MembershipRepository handles database operations for users, roles, and permissions.
|
||||
type MembershipRepository struct {
|
||||
*BaseRepository[model.User, model.MembershipFilter]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewMembershipRepository creates a new MembershipRepository.
|
||||
func NewMembershipRepository(db *gorm.DB) *MembershipRepository {
|
||||
return &MembershipRepository{
|
||||
BaseRepository: NewBaseRepository[model.User, model.MembershipFilter](db, model.User{}),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// FindUserByUsername finds a user by their username.
|
||||
// It preloads the user's role and the role's permissions.
|
||||
func (r *MembershipRepository) FindUserByUsername(ctx context.Context, username string) (*model.User, error) {
|
||||
var user model.User
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Preload("Roles.Permissions").Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindUserByIDWithPermissions finds a user by their ID and preloads Role and Permissions.
|
||||
func (r *MembershipRepository) FindUserByIDWithPermissions(ctx context.Context, userID string) (*model.User, error) {
|
||||
var user model.User
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Preload("Roles.Permissions").First(&user, "id = ?", userID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (r *MembershipRepository) CreateUser(ctx context.Context, user *model.User) error {
|
||||
db := r.db.WithContext(ctx)
|
||||
return db.Create(user).Error
|
||||
}
|
||||
|
||||
// FindRoleByName finds a role by its name.
|
||||
func (r *MembershipRepository) FindRoleByName(ctx context.Context, name string) (*model.Role, error) {
|
||||
var role model.Role
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Where("name = ?", name).First(&role).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// CreateRole creates a new role.
|
||||
func (r *MembershipRepository) CreateRole(ctx context.Context, role *model.Role) error {
|
||||
db := r.db.WithContext(ctx)
|
||||
return db.Create(role).Error
|
||||
}
|
||||
|
||||
// FindPermissionByName finds a permission by its name.
|
||||
func (r *MembershipRepository) FindPermissionByName(ctx context.Context, name string) (*model.Permission, error) {
|
||||
var permission model.Permission
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Where("name = ?", name).First(&permission).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &permission, nil
|
||||
}
|
||||
|
||||
// CreatePermission creates a new permission.
|
||||
func (r *MembershipRepository) CreatePermission(ctx context.Context, permission *model.Permission) error {
|
||||
db := r.db.WithContext(ctx)
|
||||
return db.Create(permission).Error
|
||||
}
|
||||
|
||||
// AssignPermissionsToRole assigns a set of permissions to a role.
|
||||
func (r *MembershipRepository) AssignPermissionsToRole(ctx context.Context, role *model.Role, permissions []model.Permission) error {
|
||||
db := r.db.WithContext(ctx)
|
||||
return db.Model(role).Association("Permissions").Replace(permissions)
|
||||
}
|
||||
|
||||
// GetUserPermissions retrieves all permissions for a given user ID.
|
||||
func (r *MembershipRepository) GetUserPermissions(ctx context.Context, userID uuid.UUID) ([]string, error) {
|
||||
var user model.User
|
||||
db := r.db.WithContext(ctx)
|
||||
|
||||
if err := db.Preload("Roles.Permissions").First(&user, "id = ?", userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permissionSet := make(map[string]bool)
|
||||
for _, role := range user.Roles {
|
||||
for _, permission := range role.Permissions {
|
||||
permissionSet[permission.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
permissions := make([]string, 0, len(permissionSet))
|
||||
for permission := range permissionSet {
|
||||
permissions = append(permissions, permission)
|
||||
}
|
||||
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
// ListUsers retrieves all users.
|
||||
func (r *MembershipRepository) ListUsers(ctx context.Context) ([]*model.User, error) {
|
||||
var users []*model.User
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Preload("Roles").Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user.
|
||||
func (r *MembershipRepository) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
||||
db := r.db.WithContext(ctx)
|
||||
return db.Delete(&model.User{}, "id = ?", userID).Error
|
||||
}
|
||||
|
||||
// FindUserByID finds a user by their ID.
|
||||
func (r *MembershipRepository) FindUserByID(ctx context.Context, userID uuid.UUID) (*model.User, error) {
|
||||
var user model.User
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Preload("Roles").First(&user, "id = ?", userID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates a user's details in the database.
|
||||
func (r *MembershipRepository) UpdateUser(ctx context.Context, user *model.User) error {
|
||||
db := r.db.WithContext(ctx)
|
||||
return db.Save(user).Error
|
||||
}
|
||||
|
||||
// FindRoleByID finds a role by its ID.
|
||||
func (r *MembershipRepository) FindRoleByID(ctx context.Context, roleID uuid.UUID) (*model.Role, error) {
|
||||
var role model.Role
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.First(&role, "id = ?", roleID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// ListUsersWithFilter retrieves users based on the membership filter.
|
||||
func (r *MembershipRepository) ListUsersWithFilter(ctx context.Context, filter *model.MembershipFilter) (*[]model.User, error) {
|
||||
return r.BaseRepository.GetAll(ctx, filter)
|
||||
}
|
||||
|
||||
// ListRoles retrieves all roles.
|
||||
func (r *MembershipRepository) ListRoles(ctx context.Context) ([]*model.Role, error) {
|
||||
var roles []*model.Role
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Find(&roles).Error
|
||||
return roles, err
|
||||
}
|
||||
14
local/repository/repository.go
Normal file
14
local/repository/repository.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// InitializeRepositories
|
||||
// Initializes Dependency Injection modules for repositories
|
||||
//
|
||||
// Args:
|
||||
// *dig.Container: Dig Container
|
||||
func InitializeRepositories(c *dig.Container) {
|
||||
c.Provide(NewMembershipRepository)
|
||||
}
|
||||
34
local/service/api.go
Normal file
34
local/service/api.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"omega-server/local/repository"
|
||||
)
|
||||
|
||||
// ApiService provides API-related business logic
|
||||
type ApiService struct {
|
||||
// Add repository dependencies as needed
|
||||
repo *repository.BaseRepository[any, any] // Placeholder
|
||||
}
|
||||
|
||||
// NewApiService creates a new ApiService
|
||||
func NewApiService() *ApiService {
|
||||
return &ApiService{
|
||||
// Initialize with required dependencies
|
||||
}
|
||||
}
|
||||
|
||||
// SetServerService sets the server service reference (for cross-service communication)
|
||||
func (s *ApiService) SetServerService(serverService interface{}) {
|
||||
// TODO: Implement when ServerService is available
|
||||
}
|
||||
|
||||
// GetApiInfo returns basic API information
|
||||
func (s *ApiService) GetApiInfo() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": "Omega Server API",
|
||||
"version": "1.0.0",
|
||||
"status": "active",
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add other API service methods as needed
|
||||
307
local/service/membership.go
Normal file
307
local/service/membership.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"omega-server/local/model"
|
||||
"omega-server/local/repository"
|
||||
"omega-server/local/utl/jwt"
|
||||
"omega-server/local/utl/logging"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CacheInvalidator interface for cache invalidation
|
||||
type CacheInvalidator interface {
|
||||
InvalidateUserPermissions(userID string)
|
||||
InvalidateAllUserPermissions()
|
||||
}
|
||||
|
||||
// MembershipService provides business logic for membership-related operations.
|
||||
type MembershipService struct {
|
||||
repo *repository.MembershipRepository
|
||||
cacheInvalidator CacheInvalidator
|
||||
}
|
||||
|
||||
// NewMembershipService creates a new MembershipService.
|
||||
func NewMembershipService(repo *repository.MembershipRepository) *MembershipService {
|
||||
return &MembershipService{
|
||||
repo: repo,
|
||||
cacheInvalidator: nil, // Will be set later via SetCacheInvalidator
|
||||
}
|
||||
}
|
||||
|
||||
// SetCacheInvalidator sets the cache invalidator after service initialization
|
||||
func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
|
||||
s.cacheInvalidator = invalidator
|
||||
}
|
||||
|
||||
// Login authenticates a user and returns a JWT.
|
||||
func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) {
|
||||
user, err := s.repo.FindUserByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Use secure password verification with constant-time comparison
|
||||
if !user.VerifyPassword(password) {
|
||||
return "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Extract role names for JWT
|
||||
roleNames := make([]string, len(user.Roles))
|
||||
for i, role := range user.Roles {
|
||||
roleNames[i] = role.Name
|
||||
}
|
||||
|
||||
return jwt.GenerateToken(user.ID, user.Email, user.Username, roleNames)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (s *MembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) {
|
||||
|
||||
role, err := s.repo.FindRoleByName(ctx, roleName)
|
||||
if err != nil {
|
||||
logging.Error("Failed to find role by name: %v", err)
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
Username: username,
|
||||
Email: username + "@example.com", // You may want to accept email as parameter
|
||||
Name: username,
|
||||
}
|
||||
|
||||
// Set password using the model's SetPassword method
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Assign roles
|
||||
user.Roles = []model.Role{*role}
|
||||
|
||||
if err := s.repo.CreateUser(ctx, user); err != nil {
|
||||
logging.Error("Failed to create user: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.InfoOperation("USER_CREATE", "Created user: "+user.Username+" (ID: "+user.ID+", Role: "+roleName+")")
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ListUsers retrieves all users.
|
||||
func (s *MembershipService) ListUsers(ctx context.Context) ([]*model.User, error) {
|
||||
return s.repo.ListUsers(ctx)
|
||||
}
|
||||
|
||||
// GetUser retrieves a single user by ID.
|
||||
func (s *MembershipService) GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error) {
|
||||
return s.repo.FindUserByID(ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserWithPermissions retrieves a single user by ID with their role and permissions.
|
||||
func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error) {
|
||||
return s.repo.FindUserByIDWithPermissions(ctx, userID)
|
||||
}
|
||||
|
||||
// UpdateUserRequest defines the request body for updating a user.
|
||||
type UpdateUserRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
RoleID *string `json:"roleId"`
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user with validation to prevent Super Admin deletion.
|
||||
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
||||
// Get user with role information
|
||||
user, err := s.repo.FindUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Prevent deletion of Super Admin users
|
||||
for _, role := range user.Roles {
|
||||
if role.Name == "Super Admin" {
|
||||
return errors.New("cannot delete Super Admin user")
|
||||
}
|
||||
}
|
||||
|
||||
err = s.repo.DeleteUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate cache for deleted user
|
||||
if s.cacheInvalidator != nil {
|
||||
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
||||
}
|
||||
|
||||
logging.InfoOperation("USER_DELETE", "Deleted user: "+userID.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUser updates a user's details.
|
||||
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error) {
|
||||
user, err := s.repo.FindUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if req.Username != nil {
|
||||
user.Username = *req.Username
|
||||
}
|
||||
|
||||
if req.Password != nil && *req.Password != "" {
|
||||
// Use the model's SetPassword method to hash the password
|
||||
if err := user.SetPassword(*req.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if req.RoleID != nil {
|
||||
// Check if role exists
|
||||
roleUUID, err := uuid.Parse(*req.RoleID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid role ID format")
|
||||
}
|
||||
role, err := s.repo.FindRoleByID(ctx, roleUUID)
|
||||
if err != nil {
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
user.Roles = []model.Role{*role}
|
||||
}
|
||||
|
||||
if err := s.repo.UpdateUser(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Invalidate cache if role was changed
|
||||
if req.RoleID != nil && s.cacheInvalidator != nil {
|
||||
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
||||
}
|
||||
|
||||
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Username+" (ID: "+user.ID+")")
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// HasPermission checks if a user has a specific permission.
|
||||
func (s *MembershipService) HasPermission(ctx context.Context, userID string, permissionName string) (bool, error) {
|
||||
user, err := s.repo.FindUserByIDWithPermissions(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check all user roles for permissions
|
||||
for _, role := range user.Roles {
|
||||
// Super admin and Admin have all permissions
|
||||
if role.Name == "Super Admin" || role.Name == "Admin" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for _, p := range role.Permissions {
|
||||
if p.Name == permissionName {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SetupInitialData creates the initial roles and permissions.
|
||||
func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
// Define all permissions
|
||||
permissions := model.AllPermissions
|
||||
|
||||
createdPermissions := make([]model.Permission, 0)
|
||||
for _, pName := range permissions {
|
||||
perm, err := s.repo.FindPermissionByName(ctx, pName)
|
||||
if err != nil { // Assuming error means not found
|
||||
perm = &model.Permission{Name: pName}
|
||||
if err := s.repo.CreatePermission(ctx, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
createdPermissions = append(createdPermissions, *perm)
|
||||
}
|
||||
|
||||
// Create Super Admin role with all permissions
|
||||
superAdminRole, err := s.repo.FindRoleByName(ctx, "Super Admin")
|
||||
if err != nil {
|
||||
superAdminRole = &model.Role{Name: "Super Admin"}
|
||||
if err := s.repo.CreateRole(ctx, superAdminRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.repo.AssignPermissionsToRole(ctx, superAdminRole, createdPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create Admin role with same permissions as Super Admin
|
||||
adminRole, err := s.repo.FindRoleByName(ctx, "Admin")
|
||||
if err != nil {
|
||||
adminRole = &model.Role{Name: "Admin"}
|
||||
if err := s.repo.CreateRole(ctx, adminRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.repo.AssignPermissionsToRole(ctx, adminRole, createdPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create Manager role with limited permissions (excluding membership, role, user, server create/delete)
|
||||
managerRole, err := s.repo.FindRoleByName(ctx, "Manager")
|
||||
if err != nil {
|
||||
managerRole = &model.Role{Name: "Manager"}
|
||||
if err := s.repo.CreateRole(ctx, managerRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Define manager permissions (limited set)
|
||||
managerPermissionNames := []string{
|
||||
model.ServerView,
|
||||
model.ServerUpdate,
|
||||
model.ServerStart,
|
||||
model.ServerStop,
|
||||
model.ConfigView,
|
||||
model.ConfigUpdate,
|
||||
}
|
||||
|
||||
managerPermissions := make([]model.Permission, 0)
|
||||
for _, permName := range managerPermissionNames {
|
||||
for _, perm := range createdPermissions {
|
||||
if perm.Name == permName {
|
||||
managerPermissions = append(managerPermissions, perm)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.repo.AssignPermissionsToRole(ctx, managerRole, managerPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate all caches after role setup changes
|
||||
if s.cacheInvalidator != nil {
|
||||
s.cacheInvalidator.InvalidateAllUserPermissions()
|
||||
}
|
||||
|
||||
// Create a default admin user if one doesn't exist
|
||||
_, err = s.repo.FindUserByUsername(ctx, "admin")
|
||||
if err != nil {
|
||||
logging.Debug("Creating default admin user")
|
||||
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllRoles retrieves all roles for dropdown selection.
|
||||
func (s *MembershipService) GetAllRoles(ctx context.Context) ([]*model.Role, error) {
|
||||
return s.repo.ListRoles(ctx)
|
||||
}
|
||||
28
local/service/membership_interface.go
Normal file
28
local/service/membership_interface.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"omega-server/local/model"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MembershipServiceInterface defines the interface for membership-related operations
|
||||
type MembershipServiceInterface interface {
|
||||
// Authentication and Authorization
|
||||
Login(ctx context.Context, username, password string) (string, error)
|
||||
HasPermission(ctx context.Context, userID string, permissionName string) (bool, error)
|
||||
GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error)
|
||||
SetCacheInvalidator(invalidator CacheInvalidator)
|
||||
|
||||
// User Management
|
||||
CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error)
|
||||
ListUsers(ctx context.Context) ([]*model.User, error)
|
||||
GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error)
|
||||
DeleteUser(ctx context.Context, userID uuid.UUID) error
|
||||
UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error)
|
||||
|
||||
// Role Management
|
||||
GetAllRoles(ctx context.Context) ([]*model.Role, error)
|
||||
SetupInitialData(ctx context.Context) error
|
||||
}
|
||||
24
local/service/service.go
Normal file
24
local/service/service.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"omega-server/local/repository"
|
||||
"omega-server/local/utl/logging"
|
||||
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// InitializeServices
|
||||
// Initializes Dependency Injection modules for services
|
||||
//
|
||||
// Args:
|
||||
// *dig.Container: Dig Container
|
||||
func InitializeServices(c *dig.Container) {
|
||||
logging.Debug("Initializing repositories")
|
||||
repository.InitializeRepositories(c)
|
||||
|
||||
logging.Debug("Registering services")
|
||||
// Provide services
|
||||
c.Provide(NewMembershipService)
|
||||
|
||||
logging.Debug("Completed service initialization")
|
||||
}
|
||||
102
local/utl/cache/cache.go
vendored
Normal file
102
local/utl/cache/cache.go
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"omega-server/local/utl/logging"
|
||||
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// CacheItem represents an item in the cache
|
||||
type CacheItem struct {
|
||||
Value interface{}
|
||||
Expiration int64
|
||||
}
|
||||
|
||||
// InMemoryCache is a thread-safe in-memory cache
|
||||
type InMemoryCache struct {
|
||||
items map[string]CacheItem
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewInMemoryCache creates and returns a new InMemoryCache instance
|
||||
func NewInMemoryCache() *InMemoryCache {
|
||||
return &InMemoryCache{
|
||||
items: make(map[string]CacheItem),
|
||||
}
|
||||
}
|
||||
|
||||
// Set adds an item to the cache with an expiration duration (in seconds)
|
||||
func (c *InMemoryCache) Set(key string, value interface{}, duration time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var expiration int64
|
||||
if duration > 0 {
|
||||
expiration = time.Now().Add(duration).UnixNano()
|
||||
}
|
||||
|
||||
c.items[key] = CacheItem{
|
||||
Value: value,
|
||||
Expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves an item from the cache
|
||||
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
item, found := c.items[key]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
|
||||
// Item has expired, but don't delete here to avoid lock upgrade.
|
||||
// It will be overwritten on the next Set.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
// Delete removes an item from the cache
|
||||
func (c *InMemoryCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, key)
|
||||
}
|
||||
|
||||
// GetOrSet retrieves an item from the cache. If the item is not found, it
|
||||
// calls the provided function to get the value, sets it in the cache, and
|
||||
// returns it.
|
||||
func GetOrSet[T any](c *InMemoryCache, key string, duration time.Duration, fetcher func() (T, error)) (T, error) {
|
||||
if cached, found := c.Get(key); found {
|
||||
if value, ok := cached.(T); ok {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
value, err := fetcher()
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, err
|
||||
}
|
||||
|
||||
c.Set(key, value, duration)
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Start initializes the cache and provides it to the DI container.
|
||||
func Start(di *dig.Container) {
|
||||
cache := NewInMemoryCache()
|
||||
err := di.Provide(func() *InMemoryCache {
|
||||
return cache
|
||||
})
|
||||
if err != nil {
|
||||
logging.Panic("failed to provide cache")
|
||||
}
|
||||
}
|
||||
311
local/utl/common/types.go
Normal file
311
local/utl/common/types.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// RouteGroups holds the different route groups for API organization
|
||||
type RouteGroups struct {
|
||||
API fiber.Router
|
||||
Auth fiber.Router
|
||||
Users fiber.Router
|
||||
Roles fiber.Router
|
||||
System fiber.Router
|
||||
Admin fiber.Router
|
||||
}
|
||||
|
||||
// HTTPError represents a structured HTTP error
|
||||
type HTTPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e HTTPError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewHTTPError creates a new HTTP error
|
||||
func NewHTTPError(code int, message string) HTTPError {
|
||||
return HTTPError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// NewHTTPErrorWithDetails creates a new HTTP error with details
|
||||
func NewHTTPErrorWithDetails(code int, message string, details map[string]interface{}) HTTPError {
|
||||
return HTTPError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidationError represents a validation error
|
||||
type ValidationError struct {
|
||||
Field string `json:"field"`
|
||||
Message string `json:"message"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// ValidationErrors represents multiple validation errors
|
||||
type ValidationErrors []ValidationError
|
||||
|
||||
// Error implements the error interface
|
||||
func (ve ValidationErrors) Error() string {
|
||||
if len(ve) == 0 {
|
||||
return "validation failed"
|
||||
}
|
||||
return ve[0].Message
|
||||
}
|
||||
|
||||
// APIResponse represents a standard API response structure
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// SuccessResponse creates a success response
|
||||
func SuccessResponse(data interface{}, message ...string) APIResponse {
|
||||
response := APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
if len(message) > 0 {
|
||||
response.Message = message[0]
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// ErrorResponse creates an error response
|
||||
func ErrorResponse(code int, message string) APIResponse {
|
||||
return APIResponse{
|
||||
Success: false,
|
||||
Error: message,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
// PaginationRequest represents pagination parameters
|
||||
type PaginationRequest struct {
|
||||
Page int `query:"page" validate:"min=1"`
|
||||
Limit int `query:"limit" validate:"min=1,max=100"`
|
||||
Sort string `query:"sort"`
|
||||
Order string `query:"order" validate:"oneof=asc desc"`
|
||||
Search string `query:"search"`
|
||||
Filter string `query:"filter"`
|
||||
Category string `query:"category"`
|
||||
}
|
||||
|
||||
// DefaultPagination returns default pagination values
|
||||
func DefaultPagination() PaginationRequest {
|
||||
return PaginationRequest{
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
Sort: "dateCreated",
|
||||
Order: "desc",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates pagination parameters
|
||||
func (p *PaginationRequest) Validate() {
|
||||
if p.Page < 1 {
|
||||
p.Page = 1
|
||||
}
|
||||
if p.Limit < 1 || p.Limit > 100 {
|
||||
p.Limit = 10
|
||||
}
|
||||
if p.Sort == "" {
|
||||
p.Sort = "dateCreated"
|
||||
}
|
||||
if p.Order != "asc" && p.Order != "desc" {
|
||||
p.Order = "desc"
|
||||
}
|
||||
}
|
||||
|
||||
// Offset calculates the offset for database queries
|
||||
func (p *PaginationRequest) Offset() int {
|
||||
return (p.Page - 1) * p.Limit
|
||||
}
|
||||
|
||||
// PaginationResponse represents paginated response metadata
|
||||
type PaginationResponse struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Total int64 `json:"total"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
HasPrevious bool `json:"hasPrevious"`
|
||||
NextPage *int `json:"nextPage,omitempty"`
|
||||
PreviousPage *int `json:"previousPage,omitempty"`
|
||||
}
|
||||
|
||||
// NewPaginationResponse creates a new pagination response
|
||||
func NewPaginationResponse(page, limit int, total int64) PaginationResponse {
|
||||
totalPages := int((total + int64(limit) - 1) / int64(limit))
|
||||
|
||||
response := PaginationResponse{
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
HasNext: page < totalPages,
|
||||
HasPrevious: page > 1,
|
||||
}
|
||||
|
||||
if response.HasNext {
|
||||
next := page + 1
|
||||
response.NextPage = &next
|
||||
}
|
||||
|
||||
if response.HasPrevious {
|
||||
prev := page - 1
|
||||
response.PreviousPage = &prev
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// PaginatedResponse represents a paginated API response
|
||||
type PaginatedResponse struct {
|
||||
APIResponse
|
||||
Pagination PaginationResponse `json:"pagination"`
|
||||
}
|
||||
|
||||
// NewPaginatedResponse creates a new paginated response
|
||||
func NewPaginatedResponse(data interface{}, pagination PaginationResponse, message ...string) PaginatedResponse {
|
||||
response := PaginatedResponse{
|
||||
APIResponse: SuccessResponse(data, message...),
|
||||
Pagination: pagination,
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// RequestContext represents the context of an HTTP request
|
||||
type RequestContext struct {
|
||||
UserID string
|
||||
Email string
|
||||
Username string
|
||||
Roles []string
|
||||
IP string
|
||||
UserAgent string
|
||||
RequestID string
|
||||
SessionID string
|
||||
}
|
||||
|
||||
// GetUserInfo returns user information from context
|
||||
func (rc *RequestContext) GetUserInfo() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"userId": rc.UserID,
|
||||
"email": rc.Email,
|
||||
"username": rc.Username,
|
||||
"roles": rc.Roles,
|
||||
}
|
||||
}
|
||||
|
||||
// HasRole checks if the user has a specific role
|
||||
func (rc *RequestContext) HasRole(role string) bool {
|
||||
for _, r := range rc.Roles {
|
||||
if r == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user has admin role
|
||||
func (rc *RequestContext) IsAdmin() bool {
|
||||
return rc.HasRole("admin")
|
||||
}
|
||||
|
||||
// SortDirection represents sort direction
|
||||
type SortDirection string
|
||||
|
||||
const (
|
||||
SortAsc SortDirection = "asc"
|
||||
SortDesc SortDirection = "desc"
|
||||
)
|
||||
|
||||
// SortField represents a field to sort by
|
||||
type SortField struct {
|
||||
Field string `json:"field"`
|
||||
Direction SortDirection `json:"direction"`
|
||||
}
|
||||
|
||||
// FilterOperator represents filter operators
|
||||
type FilterOperator string
|
||||
|
||||
const (
|
||||
FilterEqual FilterOperator = "eq"
|
||||
FilterNotEqual FilterOperator = "ne"
|
||||
FilterGreaterThan FilterOperator = "gt"
|
||||
FilterGreaterEqual FilterOperator = "gte"
|
||||
FilterLessThan FilterOperator = "lt"
|
||||
FilterLessEqual FilterOperator = "lte"
|
||||
FilterLike FilterOperator = "like"
|
||||
FilterIn FilterOperator = "in"
|
||||
FilterNotIn FilterOperator = "nin"
|
||||
FilterIsNull FilterOperator = "isnull"
|
||||
FilterIsNotNull FilterOperator = "isnotnull"
|
||||
)
|
||||
|
||||
// FilterCondition represents a filter condition
|
||||
type FilterCondition struct {
|
||||
Field string `json:"field"`
|
||||
Operator FilterOperator `json:"operator"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// QueryOptions represents query options for database operations
|
||||
type QueryOptions struct {
|
||||
Filters []FilterCondition `json:"filters,omitempty"`
|
||||
Sort []SortField `json:"sort,omitempty"`
|
||||
Pagination *PaginationRequest `json:"pagination,omitempty"`
|
||||
Include []string `json:"include,omitempty"`
|
||||
Exclude []string `json:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
// BulkOperation represents a bulk operation request
|
||||
type BulkOperation struct {
|
||||
Action string `json:"action" validate:"required"`
|
||||
IDs []string `json:"ids" validate:"required,min=1"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// AuditInfo represents audit information for operations
|
||||
type AuditInfo struct {
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
UserID string `json:"userId"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// HealthStatus represents system health status
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Version string `json:"version"`
|
||||
Environment string `json:"environment"`
|
||||
Uptime string `json:"uptime"`
|
||||
Database map[string]interface{} `json:"database"`
|
||||
Cache map[string]interface{} `json:"cache"`
|
||||
Memory map[string]interface{} `json:"memory"`
|
||||
}
|
||||
|
||||
// SystemStats represents system statistics
|
||||
type SystemStats struct {
|
||||
TotalUsers int64 `json:"totalUsers"`
|
||||
ActiveUsers int64 `json:"activeUsers"`
|
||||
TotalRoles int64 `json:"totalRoles"`
|
||||
TotalRequests int64 `json:"totalRequests"`
|
||||
ErrorRate float64 `json:"errorRate"`
|
||||
ResponseTime float64 `json:"avgResponseTime"`
|
||||
}
|
||||
220
local/utl/configs/config.go
Normal file
220
local/utl/configs/config.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// API configuration constants
|
||||
const (
|
||||
// Prefix for all API routes
|
||||
Prefix = "/api/v1"
|
||||
|
||||
// Default values
|
||||
DefaultPort = "3000"
|
||||
DefaultDatabaseName = "app.db"
|
||||
DefaultJWTExpiryHours = 24
|
||||
DefaultPasswordMinLen = 8
|
||||
DefaultMaxLoginAttempts = 5
|
||||
DefaultLockoutMinutes = 30
|
||||
DefaultRateLimitReqs = 100
|
||||
DefaultRateLimitWindow = 1 // minute
|
||||
)
|
||||
|
||||
// Environment variable keys
|
||||
const (
|
||||
EnvPort = "PORT"
|
||||
EnvDatabaseName = "DB_NAME"
|
||||
EnvJWTSecret = "JWT_SECRET"
|
||||
EnvAppSecret = "APP_SECRET"
|
||||
EnvAppSecretCode = "APP_SECRET_CODE"
|
||||
EnvEncryptionKey = "ENCRYPTION_KEY"
|
||||
EnvCORSAllowedOrigin = "CORS_ALLOWED_ORIGIN"
|
||||
EnvLogLevel = "LOG_LEVEL"
|
||||
EnvDebugMode = "DEBUG_MODE"
|
||||
EnvDefaultAdminPassword = "DEFAULT_ADMIN_PASSWORD"
|
||||
EnvJWTAccessTTLHours = "JWT_ACCESS_TTL_HOURS"
|
||||
EnvJWTRefreshTTLDays = "JWT_REFRESH_TTL_DAYS"
|
||||
EnvJWTIssuer = "JWT_ISSUER"
|
||||
)
|
||||
|
||||
// GetEnv returns environment variable value or default
|
||||
func GetEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvInt returns environment variable as integer or default
|
||||
func GetEnvInt(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvBool returns environment variable as boolean or default
|
||||
func GetEnvBool(key string, defaultValue bool) bool {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvDuration returns environment variable as duration or default
|
||||
func GetEnvDuration(key string, defaultValue time.Duration) time.Duration {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if duration, err := time.ParseDuration(value); err == nil {
|
||||
return duration
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Config holds application configuration
|
||||
type Config struct {
|
||||
// Server configuration
|
||||
Port string
|
||||
DatabaseName string
|
||||
LogLevel string
|
||||
DebugMode bool
|
||||
|
||||
// Security configuration
|
||||
JWTSecret string
|
||||
AppSecret string
|
||||
AppSecretCode string
|
||||
EncryptionKey string
|
||||
JWTAccessTTLHours int
|
||||
JWTRefreshTTLDays int
|
||||
JWTIssuer string
|
||||
PasswordMinLength int
|
||||
MaxLoginAttempts int
|
||||
LockoutDurationMins int
|
||||
|
||||
// CORS configuration
|
||||
CORSAllowedOrigin string
|
||||
|
||||
// Rate limiting
|
||||
RateLimitRequests int
|
||||
RateLimitWindow int // minutes
|
||||
|
||||
// Admin configuration
|
||||
DefaultAdminPassword string
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from environment variables
|
||||
func LoadConfig() *Config {
|
||||
return &Config{
|
||||
// Server configuration
|
||||
Port: GetEnv(EnvPort, DefaultPort),
|
||||
DatabaseName: GetEnv(EnvDatabaseName, DefaultDatabaseName),
|
||||
LogLevel: GetEnv(EnvLogLevel, "INFO"),
|
||||
DebugMode: GetEnvBool(EnvDebugMode, false),
|
||||
|
||||
// Security configuration
|
||||
JWTSecret: GetEnv(EnvJWTSecret, ""),
|
||||
AppSecret: GetEnv(EnvAppSecret, ""),
|
||||
AppSecretCode: GetEnv(EnvAppSecretCode, ""),
|
||||
EncryptionKey: GetEnv(EnvEncryptionKey, ""),
|
||||
JWTAccessTTLHours: GetEnvInt(EnvJWTAccessTTLHours, DefaultJWTExpiryHours),
|
||||
JWTRefreshTTLDays: GetEnvInt(EnvJWTRefreshTTLDays, 7),
|
||||
JWTIssuer: GetEnv(EnvJWTIssuer, "omega-server"),
|
||||
PasswordMinLength: GetEnvInt("PASSWORD_MIN_LENGTH", DefaultPasswordMinLen),
|
||||
MaxLoginAttempts: GetEnvInt("MAX_LOGIN_ATTEMPTS", DefaultMaxLoginAttempts),
|
||||
LockoutDurationMins: GetEnvInt("LOCKOUT_DURATION_MINUTES", DefaultLockoutMinutes),
|
||||
|
||||
// CORS configuration
|
||||
CORSAllowedOrigin: GetEnv(EnvCORSAllowedOrigin, "http://localhost:5173"),
|
||||
|
||||
// Rate limiting
|
||||
RateLimitRequests: GetEnvInt("RATE_LIMIT_REQUESTS", DefaultRateLimitReqs),
|
||||
RateLimitWindow: GetEnvInt("RATE_LIMIT_WINDOW_MINUTES", DefaultRateLimitWindow),
|
||||
|
||||
// Admin configuration
|
||||
DefaultAdminPassword: GetEnv(EnvDefaultAdminPassword, ""),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func (c *Config) Validate() []string {
|
||||
var errors []string
|
||||
|
||||
// Required security settings
|
||||
if c.JWTSecret == "" {
|
||||
errors = append(errors, "JWT_SECRET is required")
|
||||
}
|
||||
if c.AppSecret == "" {
|
||||
errors = append(errors, "APP_SECRET is required")
|
||||
}
|
||||
if c.AppSecretCode == "" {
|
||||
errors = append(errors, "APP_SECRET_CODE is required")
|
||||
}
|
||||
if c.EncryptionKey == "" {
|
||||
errors = append(errors, "ENCRYPTION_KEY is required")
|
||||
}
|
||||
|
||||
// Validate encryption key length (must be 32 characters for AES-256)
|
||||
if len(c.EncryptionKey) != 32 {
|
||||
errors = append(errors, "ENCRYPTION_KEY must be exactly 32 characters long")
|
||||
}
|
||||
|
||||
// Validate JWT settings
|
||||
if c.JWTAccessTTLHours <= 0 {
|
||||
errors = append(errors, "JWT_ACCESS_TTL_HOURS must be greater than 0")
|
||||
}
|
||||
if c.JWTRefreshTTLDays <= 0 {
|
||||
errors = append(errors, "JWT_REFRESH_TTL_DAYS must be greater than 0")
|
||||
}
|
||||
|
||||
// Validate password settings
|
||||
if c.PasswordMinLength < 8 {
|
||||
errors = append(errors, "PASSWORD_MIN_LENGTH must be at least 8")
|
||||
}
|
||||
|
||||
// Validate rate limiting
|
||||
if c.RateLimitRequests <= 0 {
|
||||
errors = append(errors, "RATE_LIMIT_REQUESTS must be greater than 0")
|
||||
}
|
||||
if c.RateLimitWindow <= 0 {
|
||||
errors = append(errors, "RATE_LIMIT_WINDOW_MINUTES must be greater than 0")
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// IsProduction returns true if running in production mode
|
||||
func (c *Config) IsProduction() bool {
|
||||
env := GetEnv("GO_ENV", "development")
|
||||
return env == "production"
|
||||
}
|
||||
|
||||
// IsDevelopment returns true if running in development mode
|
||||
func (c *Config) IsDevelopment() bool {
|
||||
return !c.IsProduction()
|
||||
}
|
||||
|
||||
// GetJWTAccessTTL returns JWT access token TTL as duration
|
||||
func (c *Config) GetJWTAccessTTL() time.Duration {
|
||||
return time.Duration(c.JWTAccessTTLHours) * time.Hour
|
||||
}
|
||||
|
||||
// GetJWTRefreshTTL returns JWT refresh token TTL as duration
|
||||
func (c *Config) GetJWTRefreshTTL() time.Duration {
|
||||
return time.Duration(c.JWTRefreshTTLDays) * 24 * time.Hour
|
||||
}
|
||||
|
||||
// GetLockoutDuration returns account lockout duration
|
||||
func (c *Config) GetLockoutDuration() time.Duration {
|
||||
return time.Duration(c.LockoutDurationMins) * time.Minute
|
||||
}
|
||||
|
||||
// GetRateLimitWindow returns rate limit window as duration
|
||||
func (c *Config) GetRateLimitWindow() time.Duration {
|
||||
return time.Duration(c.RateLimitWindow) * time.Minute
|
||||
}
|
||||
320
local/utl/db/db.go
Normal file
320
local/utl/db/db.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"omega-server/local/model"
|
||||
"omega-server/local/utl/logging"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.uber.org/dig"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func Start(di *dig.Container) {
|
||||
dbName := os.Getenv("DB_NAME")
|
||||
if dbName == "" {
|
||||
dbName = "app.db"
|
||||
}
|
||||
|
||||
// Configure GORM logger
|
||||
gormLogger := logger.Default
|
||||
if os.Getenv("LOG_LEVEL") == "DEBUG" {
|
||||
gormLogger = logger.Default.LogMode(logger.Info)
|
||||
} else {
|
||||
gormLogger = logger.Default.LogMode(logger.Silent)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
})
|
||||
if err != nil {
|
||||
logging.Panic("failed to connect database: " + err.Error())
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
logging.Panic("failed to get database instance: " + err.Error())
|
||||
}
|
||||
|
||||
// Set connection pool settings
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
err = di.Provide(func() *gorm.DB {
|
||||
return db
|
||||
})
|
||||
if err != nil {
|
||||
logging.Panic("failed to bind database: " + err.Error())
|
||||
}
|
||||
|
||||
logging.Info("Database connected successfully")
|
||||
Migrate(db)
|
||||
}
|
||||
|
||||
func Migrate(db *gorm.DB) {
|
||||
logging.Info("Starting database migration...")
|
||||
|
||||
err := db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.Role{},
|
||||
&model.Permission{},
|
||||
&model.SystemConfig{},
|
||||
&model.AuditLog{},
|
||||
&model.SecurityEvent{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logging.Panic("failed to migrate database models: " + err.Error())
|
||||
}
|
||||
|
||||
logging.Info("Database migration completed successfully")
|
||||
Seed(db)
|
||||
}
|
||||
|
||||
func Seed(db *gorm.DB) error {
|
||||
logging.Info("Starting database seeding...")
|
||||
|
||||
if err := seedRoles(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedPermissions(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedDefaultAdmin(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedSystemConfigs(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logging.Info("Database seeding completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedRoles(db *gorm.DB) error {
|
||||
roles := []model.Role{
|
||||
{Name: "admin", Description: "Administrator with full access"},
|
||||
{Name: "user", Description: "Regular user with limited access"},
|
||||
{Name: "viewer", Description: "Read-only access"},
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
var existingRole model.Role
|
||||
err := db.Where("name = ?", role.Name).First(&existingRole).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
role.Init()
|
||||
if err := db.Create(&role).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
logging.Info("Created role: %s", role.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedPermissions(db *gorm.DB) error {
|
||||
permissions := []model.Permission{
|
||||
{Name: "user:create", Description: "Create new users"},
|
||||
{Name: "user:read", Description: "Read user information"},
|
||||
{Name: "user:update", Description: "Update user information"},
|
||||
{Name: "user:delete", Description: "Delete users"},
|
||||
{Name: "role:create", Description: "Create new roles"},
|
||||
{Name: "role:read", Description: "Read role information"},
|
||||
{Name: "role:update", Description: "Update role information"},
|
||||
{Name: "role:delete", Description: "Delete roles"},
|
||||
{Name: "system:config", Description: "Access system configuration"},
|
||||
{Name: "system:logs", Description: "Access system logs"},
|
||||
{Name: "system:admin", Description: "Full system administration"},
|
||||
}
|
||||
|
||||
for _, permission := range permissions {
|
||||
var existingPermission model.Permission
|
||||
err := db.Where("name = ?", permission.Name).First(&existingPermission).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
permission.Init()
|
||||
if err := db.Create(&permission).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
logging.Info("Created permission: %s", permission.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign all permissions to admin role
|
||||
var adminRole model.Role
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var allPermissions []model.Permission
|
||||
if err := db.Find(&allPermissions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Model(&adminRole).Association("Permissions").Replace(allPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assign basic permissions to user role
|
||||
var userRole model.Role
|
||||
if err := db.Where("name = ?", "user").First(&userRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userPermissions []model.Permission
|
||||
userPermissionNames := []string{"user:read", "role:read"}
|
||||
if err := db.Where("name IN ?", userPermissionNames).Find(&userPermissions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Model(&userRole).Association("Permissions").Replace(userPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assign read permissions to viewer role
|
||||
var viewerRole model.Role
|
||||
if err := db.Where("name = ?", "viewer").First(&viewerRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var viewerPermissions []model.Permission
|
||||
viewerPermissionNames := []string{"user:read", "role:read"}
|
||||
if err := db.Where("name IN ?", viewerPermissionNames).Find(&viewerPermissions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Model(&viewerRole).Association("Permissions").Replace(viewerPermissions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedDefaultAdmin(db *gorm.DB) error {
|
||||
// Check if admin user already exists
|
||||
var existingAdmin model.User
|
||||
err := db.Where("email = ?", "admin@example.com").First(&existingAdmin).Error
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil // Admin already exists or other error
|
||||
}
|
||||
|
||||
// Get admin role
|
||||
var adminRole model.Role
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create default admin user
|
||||
defaultPassword := os.Getenv("DEFAULT_ADMIN_PASSWORD")
|
||||
if defaultPassword == "" {
|
||||
defaultPassword = "admin123"
|
||||
}
|
||||
|
||||
admin := model.User{
|
||||
Email: "admin@example.com",
|
||||
Username: "admin",
|
||||
Name: "System Administrator",
|
||||
Active: true,
|
||||
}
|
||||
admin.Init()
|
||||
|
||||
if err := admin.SetPassword(defaultPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Create(&admin).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assign admin role
|
||||
if err := db.Model(&admin).Association("Roles").Append(&adminRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logging.Info("Created default admin user with email: %s", admin.Email)
|
||||
logging.Warn("Default admin password is: %s - PLEASE CHANGE THIS IMMEDIATELY!", defaultPassword)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedSystemConfigs(db *gorm.DB) error {
|
||||
configs := []model.SystemConfig{
|
||||
{
|
||||
Key: "app.name",
|
||||
Value: "Bootstrap App",
|
||||
DefaultValue: "Bootstrap App",
|
||||
Description: "Application name",
|
||||
Category: "general",
|
||||
DataType: "string",
|
||||
IsEditable: true,
|
||||
DateModified: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
{
|
||||
Key: "app.version",
|
||||
Value: "1.0.0",
|
||||
DefaultValue: "1.0.0",
|
||||
Description: "Application version",
|
||||
Category: "general",
|
||||
DataType: "string",
|
||||
IsEditable: false,
|
||||
DateModified: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
{
|
||||
Key: "security.jwt_expiry_hours",
|
||||
Value: "24",
|
||||
DefaultValue: "24",
|
||||
Description: "JWT token expiry time in hours",
|
||||
Category: "security",
|
||||
DataType: "integer",
|
||||
IsEditable: true,
|
||||
DateModified: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
{
|
||||
Key: "security.password_min_length",
|
||||
Value: "8",
|
||||
DefaultValue: "8",
|
||||
Description: "Minimum password length",
|
||||
Category: "security",
|
||||
DataType: "integer",
|
||||
IsEditable: true,
|
||||
DateModified: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
{
|
||||
Key: "security.max_login_attempts",
|
||||
Value: "5",
|
||||
DefaultValue: "5",
|
||||
Description: "Maximum login attempts before lockout",
|
||||
Category: "security",
|
||||
DataType: "integer",
|
||||
IsEditable: true,
|
||||
DateModified: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
{
|
||||
Key: "security.lockout_duration_minutes",
|
||||
Value: "30",
|
||||
DefaultValue: "30",
|
||||
Description: "Account lockout duration in minutes",
|
||||
Category: "security",
|
||||
DataType: "integer",
|
||||
IsEditable: true,
|
||||
DateModified: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
var existingConfig model.SystemConfig
|
||||
err := db.Where("key = ?", config.Key).First(&existingConfig).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
config.Init()
|
||||
if err := db.Create(&config).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
logging.Info("Created system config: %s", config.Key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
184
local/utl/error_handler/controller_error_handler.go
Normal file
184
local/utl/error_handler/controller_error_handler.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package error_handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"omega-server/local/utl/logging"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// ControllerErrorHandler provides centralized error handling for controllers
|
||||
type ControllerErrorHandler struct {
|
||||
errorLogger *logging.ErrorLogger
|
||||
}
|
||||
|
||||
// NewControllerErrorHandler creates a new controller error handler instance
|
||||
func NewControllerErrorHandler() *ControllerErrorHandler {
|
||||
return &ControllerErrorHandler{
|
||||
errorLogger: logging.GetErrorLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorResponse represents a standardized error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code int `json:"code,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// HandleError handles controller errors with logging and standardized responses
|
||||
func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCode int, context ...string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get caller information for logging
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
file = strings.TrimPrefix(file, "acc-server-manager/")
|
||||
|
||||
// Build context string
|
||||
contextStr := ""
|
||||
if len(context) > 0 {
|
||||
contextStr = fmt.Sprintf("[%s] ", strings.Join(context, "|"))
|
||||
}
|
||||
|
||||
// Clean error message (remove null bytes)
|
||||
cleanErrorMsg := strings.ReplaceAll(err.Error(), "\x00", "")
|
||||
|
||||
// Log the error with context
|
||||
ceh.errorLogger.LogWithContext(
|
||||
fmt.Sprintf("CONTROLLER_ERROR [%s:%d]", file, line),
|
||||
"%s%s",
|
||||
contextStr,
|
||||
cleanErrorMsg,
|
||||
)
|
||||
|
||||
// Create standardized error response
|
||||
errorResponse := ErrorResponse{
|
||||
Error: cleanErrorMsg,
|
||||
Code: statusCode,
|
||||
}
|
||||
|
||||
// Add request details if available
|
||||
if c != nil {
|
||||
if errorResponse.Details == nil {
|
||||
errorResponse.Details = make(map[string]string)
|
||||
}
|
||||
errorResponse.Details["method"] = c.Method()
|
||||
errorResponse.Details["path"] = c.Path()
|
||||
errorResponse.Details["ip"] = c.IP()
|
||||
}
|
||||
|
||||
// Return appropriate response based on status code
|
||||
if statusCode >= 500 {
|
||||
// For server errors, don't expose internal details
|
||||
return c.Status(statusCode).JSON(ErrorResponse{
|
||||
Error: "Internal server error",
|
||||
Code: statusCode,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(statusCode).JSON(errorResponse)
|
||||
}
|
||||
|
||||
// HandleValidationError handles validation errors specifically
|
||||
func (ceh *ControllerErrorHandler) HandleValidationError(c *fiber.Ctx, err error, field string) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "VALIDATION", field)
|
||||
}
|
||||
|
||||
// HandleDatabaseError handles database-related errors
|
||||
func (ceh *ControllerErrorHandler) HandleDatabaseError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusInternalServerError, "DATABASE")
|
||||
}
|
||||
|
||||
// HandleAuthError handles authentication/authorization errors
|
||||
func (ceh *ControllerErrorHandler) HandleAuthError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusUnauthorized, "AUTH")
|
||||
}
|
||||
|
||||
// HandleNotFoundError handles resource not found errors
|
||||
func (ceh *ControllerErrorHandler) HandleNotFoundError(c *fiber.Ctx, resource string) error {
|
||||
err := fmt.Errorf("%s not found", resource)
|
||||
return ceh.HandleError(c, err, fiber.StatusNotFound, "NOT_FOUND")
|
||||
}
|
||||
|
||||
// HandleBusinessLogicError handles business logic errors
|
||||
func (ceh *ControllerErrorHandler) HandleBusinessLogicError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "BUSINESS_LOGIC")
|
||||
}
|
||||
|
||||
// HandleServiceError handles service layer errors
|
||||
func (ceh *ControllerErrorHandler) HandleServiceError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusInternalServerError, "SERVICE")
|
||||
}
|
||||
|
||||
// HandleParsingError handles request parsing errors
|
||||
func (ceh *ControllerErrorHandler) HandleParsingError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "PARSING")
|
||||
}
|
||||
|
||||
// HandleUUIDError handles UUID parsing errors
|
||||
func (ceh *ControllerErrorHandler) HandleUUIDError(c *fiber.Ctx, field string) error {
|
||||
err := fmt.Errorf("invalid %s format", field)
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "UUID_VALIDATION", field)
|
||||
}
|
||||
|
||||
// Global controller error handler instance
|
||||
var globalErrorHandler *ControllerErrorHandler
|
||||
|
||||
// GetControllerErrorHandler returns the global controller error handler instance
|
||||
func GetControllerErrorHandler() *ControllerErrorHandler {
|
||||
if globalErrorHandler == nil {
|
||||
globalErrorHandler = NewControllerErrorHandler()
|
||||
}
|
||||
return globalErrorHandler
|
||||
}
|
||||
|
||||
// Convenience functions using the global error handler
|
||||
|
||||
// HandleError handles controller errors using the global error handler
|
||||
func HandleError(c *fiber.Ctx, err error, statusCode int, context ...string) error {
|
||||
return GetControllerErrorHandler().HandleError(c, err, statusCode, context...)
|
||||
}
|
||||
|
||||
// HandleValidationError handles validation errors using the global error handler
|
||||
func HandleValidationError(c *fiber.Ctx, err error, field string) error {
|
||||
return GetControllerErrorHandler().HandleValidationError(c, err, field)
|
||||
}
|
||||
|
||||
// HandleDatabaseError handles database errors using the global error handler
|
||||
func HandleDatabaseError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleDatabaseError(c, err)
|
||||
}
|
||||
|
||||
// HandleAuthError handles auth errors using the global error handler
|
||||
func HandleAuthError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleAuthError(c, err)
|
||||
}
|
||||
|
||||
// HandleNotFoundError handles not found errors using the global error handler
|
||||
func HandleNotFoundError(c *fiber.Ctx, resource string) error {
|
||||
return GetControllerErrorHandler().HandleNotFoundError(c, resource)
|
||||
}
|
||||
|
||||
// HandleBusinessLogicError handles business logic errors using the global error handler
|
||||
func HandleBusinessLogicError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleBusinessLogicError(c, err)
|
||||
}
|
||||
|
||||
// HandleServiceError handles service errors using the global error handler
|
||||
func HandleServiceError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
// HandleParsingError handles parsing errors using the global error handler
|
||||
func HandleParsingError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleParsingError(c, err)
|
||||
}
|
||||
|
||||
// HandleUUIDError handles UUID errors using the global error handler
|
||||
func HandleUUIDError(c *fiber.Ctx, field string) error {
|
||||
return GetControllerErrorHandler().HandleUUIDError(c, field)
|
||||
}
|
||||
365
local/utl/jwt/jwt.go
Normal file
365
local/utl/jwt/jwt.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// Claims represents the JWT claims
|
||||
type Claims struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Roles []string `json:"roles"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// TokenPair represents access and refresh tokens
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
TokenType string `json:"tokenType"`
|
||||
}
|
||||
|
||||
var (
|
||||
jwtSecret []byte
|
||||
accessTokenTTL time.Duration
|
||||
refreshTokenTTL time.Duration
|
||||
issuer string
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrExpiredToken = errors.New("token has expired")
|
||||
ErrInvalidClaims = errors.New("invalid token claims")
|
||||
)
|
||||
|
||||
// Initialize initializes the JWT package with configuration
|
||||
func Initialize() error {
|
||||
// Get JWT secret from environment
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
return errors.New("JWT_SECRET environment variable is required")
|
||||
}
|
||||
jwtSecret = []byte(secret)
|
||||
|
||||
// Get token TTL from environment or use defaults
|
||||
accessTTLStr := os.Getenv("JWT_ACCESS_TTL_HOURS")
|
||||
if accessTTLStr == "" {
|
||||
accessTTLStr = "24" // 24 hours default
|
||||
}
|
||||
accessHours, err := strconv.Atoi(accessTTLStr)
|
||||
if err != nil {
|
||||
accessHours = 24
|
||||
}
|
||||
accessTokenTTL = time.Duration(accessHours) * time.Hour
|
||||
|
||||
refreshTTLStr := os.Getenv("JWT_REFRESH_TTL_DAYS")
|
||||
if refreshTTLStr == "" {
|
||||
refreshTTLStr = "7" // 7 days default
|
||||
}
|
||||
refreshDays, err := strconv.Atoi(refreshTTLStr)
|
||||
if err != nil {
|
||||
refreshDays = 7
|
||||
}
|
||||
refreshTokenTTL = time.Duration(refreshDays) * 24 * time.Hour
|
||||
|
||||
// Get issuer from environment
|
||||
issuer = os.Getenv("JWT_ISSUER")
|
||||
if issuer == "" {
|
||||
issuer = "omega-server"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateTokenPair generates both access and refresh tokens
|
||||
func GenerateTokenPair(userID, email, username string, roles []string) (*TokenPair, error) {
|
||||
if len(jwtSecret) == 0 {
|
||||
if err := Initialize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
accessExpiresAt := now.Add(accessTokenTTL)
|
||||
refreshExpiresAt := now.Add(refreshTokenTTL)
|
||||
|
||||
// Create access token claims
|
||||
accessClaims := &Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Username: username,
|
||||
Roles: roles,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(accessExpiresAt),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Issuer: issuer,
|
||||
Subject: userID,
|
||||
ID: generateJTI(),
|
||||
},
|
||||
}
|
||||
|
||||
// Create refresh token claims (minimal data)
|
||||
refreshClaims := &Claims{
|
||||
UserID: userID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(refreshExpiresAt),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Issuer: issuer,
|
||||
Subject: userID,
|
||||
ID: generateJTI(),
|
||||
},
|
||||
}
|
||||
|
||||
// Generate access token
|
||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||
accessTokenString, err := accessToken.SignedString(jwtSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate refresh token
|
||||
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||
refreshTokenString, err := refreshToken.SignedString(jwtSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TokenPair{
|
||||
AccessToken: accessTokenString,
|
||||
RefreshToken: refreshTokenString,
|
||||
ExpiresAt: accessExpiresAt,
|
||||
TokenType: "Bearer",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the claims
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
if len(jwtSecret) == 0 {
|
||||
if err := Initialize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
// Additional validation
|
||||
if claims.UserID == "" {
|
||||
return nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// RefreshToken generates a new access token from a valid refresh token
|
||||
func RefreshToken(refreshTokenString string) (*TokenPair, error) {
|
||||
// Validate refresh token
|
||||
claims, err := ValidateToken(refreshTokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For refresh tokens, we only have minimal user data
|
||||
// In a real application, you might want to fetch fresh user data from the database
|
||||
return GenerateTokenPair(claims.UserID, claims.Email, claims.Username, claims.Roles)
|
||||
}
|
||||
|
||||
// ExtractTokenFromHeader extracts JWT token from Authorization header
|
||||
func ExtractTokenFromHeader(authHeader string) (string, error) {
|
||||
if authHeader == "" {
|
||||
return "", errors.New("authorization header is required")
|
||||
}
|
||||
|
||||
const bearerPrefix = "Bearer "
|
||||
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
|
||||
return "", errors.New("invalid authorization header format")
|
||||
}
|
||||
|
||||
token := authHeader[len(bearerPrefix):]
|
||||
if token == "" {
|
||||
return "", errors.New("token is required")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// IsTokenExpired checks if a token is expired without validating signature
|
||||
func IsTokenExpired(tokenString string) bool {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return claims.ExpiresAt.Before(time.Now())
|
||||
}
|
||||
|
||||
// GetTokenClaims extracts claims from token without validating signature (use carefully)
|
||||
func GetTokenClaims(tokenString string) (*Claims, error) {
|
||||
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &Claims{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
return nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// RevokeToken adds token to revocation list (implement with your chosen storage)
|
||||
func RevokeToken(tokenString string) error {
|
||||
// In a real implementation, you would store revoked tokens in a database or cache
|
||||
// with their JTI (JWT ID) and expiration time for efficient cleanup
|
||||
|
||||
claims, err := GetTokenClaims(tokenString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store the JTI in your revocation storage
|
||||
// Example: revokedTokens[claims.ID] = claims.ExpiresAt
|
||||
|
||||
_ = claims // Placeholder to avoid unused variable error
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTokenRevoked checks if a token has been revoked (implement with your chosen storage)
|
||||
func IsTokenRevoked(tokenString string) bool {
|
||||
// In a real implementation, check if the token's JTI is in the revocation list
|
||||
|
||||
claims, err := GetTokenClaims(tokenString)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check revocation storage
|
||||
// Example: _, revoked := revokedTokens[claims.ID]
|
||||
// return revoked
|
||||
|
||||
_ = claims // Placeholder to avoid unused variable error
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// generateJTI generates a unique JWT ID
|
||||
func generateJTI() string {
|
||||
// In a real implementation, use a proper UUID generator
|
||||
return strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
}
|
||||
|
||||
// GetTokenExpirationTime returns the expiration time of a token
|
||||
func GetTokenExpirationTime(tokenString string) (time.Time, error) {
|
||||
claims, err := GetTokenClaims(tokenString)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
if claims.ExpiresAt == nil {
|
||||
return time.Time{}, errors.New("token has no expiration time")
|
||||
}
|
||||
|
||||
return claims.ExpiresAt.Time, nil
|
||||
}
|
||||
|
||||
// GetTokenRemainingTime returns the remaining time before token expires
|
||||
func GetTokenRemainingTime(tokenString string) (time.Duration, error) {
|
||||
expirationTime, err := GetTokenExpirationTime(tokenString)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
remaining := time.Until(expirationTime)
|
||||
if remaining < 0 {
|
||||
return 0, ErrExpiredToken
|
||||
}
|
||||
|
||||
return remaining, nil
|
||||
}
|
||||
|
||||
// HasRole checks if the token claims contain a specific role
|
||||
func (c *Claims) HasRole(role string) bool {
|
||||
for _, r := range c.Roles {
|
||||
if r == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAnyRole checks if the token claims contain any of the specified roles
|
||||
func (c *Claims) HasAnyRole(roles []string) bool {
|
||||
for _, role := range roles {
|
||||
if c.HasRole(role) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAllRoles checks if the token claims contain all of the specified roles
|
||||
func (c *Claims) HasAllRoles(roles []string) bool {
|
||||
for _, role := range roles {
|
||||
if !c.HasRole(role) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user has admin role
|
||||
func (c *Claims) IsAdmin() bool {
|
||||
return c.HasRole("admin")
|
||||
}
|
||||
|
||||
// GetUserInfo returns basic user information from claims
|
||||
func (c *Claims) GetUserInfo() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"userId": c.UserID,
|
||||
"email": c.Email,
|
||||
"username": c.Username,
|
||||
"roles": c.Roles,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateToken generates a JWT token for a user (backward compatibility)
|
||||
func GenerateToken(userID, email, username string, roles []string) (string, error) {
|
||||
tokenPair, err := GenerateTokenPair(userID, email, username, roles)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tokenPair.AccessToken, nil
|
||||
}
|
||||
167
local/utl/logging/base.go
Normal file
167
local/utl/logging/base.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
timeFormat = "2006-01-02 15:04:05.000"
|
||||
)
|
||||
|
||||
// BaseLogger provides the core logging functionality
|
||||
type BaseLogger struct {
|
||||
file *os.File
|
||||
logger *log.Logger
|
||||
mu sync.RWMutex
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// LogLevel represents different logging levels
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
LogLevelError LogLevel = "ERROR"
|
||||
LogLevelWarn LogLevel = "WARN"
|
||||
LogLevelInfo LogLevel = "INFO"
|
||||
LogLevelDebug LogLevel = "DEBUG"
|
||||
LogLevelPanic LogLevel = "PANIC"
|
||||
)
|
||||
|
||||
// Initialize creates a new base logger instance
|
||||
func InitializeBase(tp string) (*BaseLogger, error) {
|
||||
return newBaseLogger(tp)
|
||||
}
|
||||
|
||||
func newBaseLogger(tp string) (*BaseLogger, error) {
|
||||
// Ensure logs directory exists
|
||||
if err := os.MkdirAll("logs", 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %v", err)
|
||||
}
|
||||
|
||||
// Open log file with date in name
|
||||
logPath := filepath.Join("logs", fmt.Sprintf("acc-server-%s-%s.log", time.Now().Format("2006-01-02"), tp))
|
||||
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %v", err)
|
||||
}
|
||||
|
||||
// Create multi-writer for both file and console
|
||||
multiWriter := io.MultiWriter(file, os.Stdout)
|
||||
|
||||
// Create base logger
|
||||
logger := &BaseLogger{
|
||||
file: file,
|
||||
logger: log.New(multiWriter, "", 0),
|
||||
initialized: true,
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// GetBaseLogger creates and returns a new base logger instance
|
||||
func GetBaseLogger(tp string) *BaseLogger {
|
||||
baseLogger, _ := InitializeBase(tp)
|
||||
return baseLogger
|
||||
}
|
||||
|
||||
// Close closes the log file
|
||||
func (bl *BaseLogger) Close() error {
|
||||
bl.mu.Lock()
|
||||
defer bl.mu.Unlock()
|
||||
|
||||
if bl.file != nil {
|
||||
return bl.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log writes a log entry with the specified level
|
||||
func (bl *BaseLogger) Log(level LogLevel, format string, v ...interface{}) {
|
||||
if bl == nil || !bl.initialized {
|
||||
return
|
||||
}
|
||||
|
||||
bl.mu.RLock()
|
||||
defer bl.mu.RUnlock()
|
||||
|
||||
// Get caller info (skip 2 frames: this function and the calling Log function)
|
||||
_, file, line, _ := runtime.Caller(2)
|
||||
file = filepath.Base(file)
|
||||
|
||||
// Format message
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
|
||||
// Format final log line
|
||||
logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s",
|
||||
time.Now().Format(timeFormat),
|
||||
string(level),
|
||||
file,
|
||||
line,
|
||||
msg,
|
||||
)
|
||||
|
||||
bl.logger.Println(logLine)
|
||||
}
|
||||
|
||||
// LogWithCaller writes a log entry with custom caller depth
|
||||
func (bl *BaseLogger) LogWithCaller(level LogLevel, callerDepth int, format string, v ...interface{}) {
|
||||
if bl == nil || !bl.initialized {
|
||||
return
|
||||
}
|
||||
|
||||
bl.mu.RLock()
|
||||
defer bl.mu.RUnlock()
|
||||
|
||||
// Get caller info with custom depth
|
||||
_, file, line, _ := runtime.Caller(callerDepth)
|
||||
file = filepath.Base(file)
|
||||
|
||||
// Format message
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
|
||||
// Format final log line
|
||||
logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s",
|
||||
time.Now().Format(timeFormat),
|
||||
string(level),
|
||||
file,
|
||||
line,
|
||||
msg,
|
||||
)
|
||||
|
||||
bl.logger.Println(logLine)
|
||||
}
|
||||
|
||||
// IsInitialized returns whether the base logger is initialized
|
||||
func (bl *BaseLogger) IsInitialized() bool {
|
||||
if bl == nil {
|
||||
return false
|
||||
}
|
||||
bl.mu.RLock()
|
||||
defer bl.mu.RUnlock()
|
||||
return bl.initialized
|
||||
}
|
||||
|
||||
// RecoverAndLog recovers from panics and logs them
|
||||
func RecoverAndLog() {
|
||||
baseLogger := GetBaseLogger("panic")
|
||||
if baseLogger != nil && baseLogger.IsInitialized() {
|
||||
if r := recover(); r != nil {
|
||||
// Get stack trace
|
||||
buf := make([]byte, 4096)
|
||||
n := runtime.Stack(buf, false)
|
||||
stackTrace := string(buf[:n])
|
||||
|
||||
baseLogger.LogWithCaller(LogLevelPanic, 2, "Recovered from panic: %v\nStack Trace:\n%s", r, stackTrace)
|
||||
|
||||
// Re-panic to maintain original behavior if needed
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
159
local/utl/logging/debug.go
Normal file
159
local/utl/logging/debug.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DebugLogger handles debug-level logging
|
||||
type DebugLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewDebugLogger creates a new debug logger instance
|
||||
func NewDebugLogger() *DebugLogger {
|
||||
base, _ := InitializeBase("debug")
|
||||
return &DebugLogger{
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes a debug-level log entry
|
||||
func (dl *DebugLogger) Log(format string, v ...interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes a debug-level log entry with additional context
|
||||
func (dl *DebugLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if dl.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
dl.base.Log(LogLevelDebug, contextualFormat, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogFunction logs function entry and exit for debugging
|
||||
func (dl *DebugLogger) LogFunction(functionName string, args ...interface{}) {
|
||||
if dl.base != nil {
|
||||
if len(args) > 0 {
|
||||
dl.base.Log(LogLevelDebug, "FUNCTION [%s] called with args: %+v", functionName, args)
|
||||
} else {
|
||||
dl.base.Log(LogLevelDebug, "FUNCTION [%s] called", functionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogVariable logs variable values for debugging
|
||||
func (dl *DebugLogger) LogVariable(varName string, value interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "VARIABLE [%s]: %+v", varName, value)
|
||||
}
|
||||
}
|
||||
|
||||
// LogState logs application state information
|
||||
func (dl *DebugLogger) LogState(component string, state interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "STATE [%s]: %+v", component, state)
|
||||
}
|
||||
}
|
||||
|
||||
// LogSQL logs SQL queries for debugging
|
||||
func (dl *DebugLogger) LogSQL(query string, args ...interface{}) {
|
||||
if dl.base != nil {
|
||||
if len(args) > 0 {
|
||||
dl.base.Log(LogLevelDebug, "SQL: %s | Args: %+v", query, args)
|
||||
} else {
|
||||
dl.base.Log(LogLevelDebug, "SQL: %s", query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogMemory logs memory usage information
|
||||
func (dl *DebugLogger) LogMemory() {
|
||||
if dl.base != nil {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
dl.base.Log(LogLevelDebug, "MEMORY: Alloc = %d KB, TotalAlloc = %d KB, Sys = %d KB, NumGC = %d",
|
||||
bToKb(m.Alloc), bToKb(m.TotalAlloc), bToKb(m.Sys), m.NumGC)
|
||||
}
|
||||
}
|
||||
|
||||
// LogGoroutines logs current number of goroutines
|
||||
func (dl *DebugLogger) LogGoroutines() {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "GOROUTINES: %d active", runtime.NumGoroutine())
|
||||
}
|
||||
}
|
||||
|
||||
// LogTiming logs timing information for performance debugging
|
||||
func (dl *DebugLogger) LogTiming(operation string, duration interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "TIMING [%s]: %v", operation, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert bytes to kilobytes
|
||||
func bToKb(b uint64) uint64 {
|
||||
return b / 1024
|
||||
}
|
||||
|
||||
// Global debug logger instance
|
||||
var (
|
||||
debugLogger *DebugLogger
|
||||
debugOnce sync.Once
|
||||
)
|
||||
|
||||
// GetDebugLogger returns the global debug logger instance
|
||||
func GetDebugLogger() *DebugLogger {
|
||||
debugOnce.Do(func() {
|
||||
debugLogger = NewDebugLogger()
|
||||
})
|
||||
return debugLogger
|
||||
}
|
||||
|
||||
// Debug logs a debug-level message using the global debug logger
|
||||
func Debug(format string, v ...interface{}) {
|
||||
GetDebugLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// DebugWithContext logs a debug-level message with context using the global debug logger
|
||||
func DebugWithContext(context string, format string, v ...interface{}) {
|
||||
GetDebugLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// DebugFunction logs function entry and exit using the global debug logger
|
||||
func DebugFunction(functionName string, args ...interface{}) {
|
||||
GetDebugLogger().LogFunction(functionName, args...)
|
||||
}
|
||||
|
||||
// DebugVariable logs variable values using the global debug logger
|
||||
func DebugVariable(varName string, value interface{}) {
|
||||
GetDebugLogger().LogVariable(varName, value)
|
||||
}
|
||||
|
||||
// DebugState logs application state information using the global debug logger
|
||||
func DebugState(component string, state interface{}) {
|
||||
GetDebugLogger().LogState(component, state)
|
||||
}
|
||||
|
||||
// DebugSQL logs SQL queries using the global debug logger
|
||||
func DebugSQL(query string, args ...interface{}) {
|
||||
GetDebugLogger().LogSQL(query, args...)
|
||||
}
|
||||
|
||||
// DebugMemory logs memory usage information using the global debug logger
|
||||
func DebugMemory() {
|
||||
GetDebugLogger().LogMemory()
|
||||
}
|
||||
|
||||
// DebugGoroutines logs current number of goroutines using the global debug logger
|
||||
func DebugGoroutines() {
|
||||
GetDebugLogger().LogGoroutines()
|
||||
}
|
||||
|
||||
// DebugTiming logs timing information using the global debug logger
|
||||
func DebugTiming(operation string, duration interface{}) {
|
||||
GetDebugLogger().LogTiming(operation, duration)
|
||||
}
|
||||
106
local/utl/logging/error.go
Normal file
106
local/utl/logging/error.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ErrorLogger handles error-level logging
|
||||
type ErrorLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewErrorLogger creates a new error logger instance
|
||||
func NewErrorLogger() *ErrorLogger {
|
||||
base, _ := InitializeBase("error")
|
||||
return &ErrorLogger{
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes an error-level log entry
|
||||
func (el *ErrorLogger) Log(format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
el.base.Log(LogLevelError, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes an error-level log entry with additional context
|
||||
func (el *ErrorLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
el.base.Log(LogLevelError, contextualFormat, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError logs an error object with optional message
|
||||
func (el *ErrorLogger) LogError(err error, message ...string) {
|
||||
if el.base != nil && err != nil {
|
||||
if len(message) > 0 {
|
||||
el.base.Log(LogLevelError, "%s: %v", message[0], err)
|
||||
} else {
|
||||
el.base.Log(LogLevelError, "Error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithStackTrace logs an error with stack trace
|
||||
func (el *ErrorLogger) LogWithStackTrace(format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
// Get stack trace
|
||||
buf := make([]byte, 4096)
|
||||
n := runtime.Stack(buf, false)
|
||||
stackTrace := string(buf[:n])
|
||||
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
el.base.Log(LogLevelError, "%s\nStack Trace:\n%s", msg, stackTrace)
|
||||
}
|
||||
}
|
||||
|
||||
// LogFatal logs a fatal error and exits the program
|
||||
func (el *ErrorLogger) LogFatal(format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
el.base.Log(LogLevelError, "[FATAL] "+format, v...)
|
||||
panic(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Global error logger instance
|
||||
var (
|
||||
errorLogger *ErrorLogger
|
||||
errorOnce sync.Once
|
||||
)
|
||||
|
||||
// GetErrorLogger returns the global error logger instance
|
||||
func GetErrorLogger() *ErrorLogger {
|
||||
errorOnce.Do(func() {
|
||||
errorLogger = NewErrorLogger()
|
||||
})
|
||||
return errorLogger
|
||||
}
|
||||
|
||||
// Error logs an error-level message using the global error logger
|
||||
func Error(format string, v ...interface{}) {
|
||||
GetErrorLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// ErrorWithContext logs an error-level message with context using the global error logger
|
||||
func ErrorWithContext(context string, format string, v ...interface{}) {
|
||||
GetErrorLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// LogError logs an error object using the global error logger
|
||||
func LogError(err error, message ...string) {
|
||||
GetErrorLogger().LogError(err, message...)
|
||||
}
|
||||
|
||||
// ErrorWithStackTrace logs an error with stack trace using the global error logger
|
||||
func ErrorWithStackTrace(format string, v ...interface{}) {
|
||||
GetErrorLogger().LogWithStackTrace(format, v...)
|
||||
}
|
||||
|
||||
// Fatal logs a fatal error and exits the program using the global error logger
|
||||
func Fatal(format string, v ...interface{}) {
|
||||
GetErrorLogger().LogFatal(format, v...)
|
||||
}
|
||||
130
local/utl/logging/info.go
Normal file
130
local/utl/logging/info.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// InfoLogger handles info-level logging
|
||||
type InfoLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewInfoLogger creates a new info logger instance
|
||||
func NewInfoLogger() *InfoLogger {
|
||||
base, _ := InitializeBase("info")
|
||||
return &InfoLogger{
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes an info-level log entry
|
||||
func (il *InfoLogger) Log(format string, v ...interface{}) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes an info-level log entry with additional context
|
||||
func (il *InfoLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if il.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
il.base.Log(LogLevelInfo, contextualFormat, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogStartup logs application startup information
|
||||
func (il *InfoLogger) LogStartup(component string, message string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "STARTUP [%s]: %s", component, message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogShutdown logs application shutdown information
|
||||
func (il *InfoLogger) LogShutdown(component string, message string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "SHUTDOWN [%s]: %s", component, message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogOperation logs general operation information
|
||||
func (il *InfoLogger) LogOperation(operation string, details string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "OPERATION [%s]: %s", operation, details)
|
||||
}
|
||||
}
|
||||
|
||||
// LogStatus logs status changes or updates
|
||||
func (il *InfoLogger) LogStatus(component string, status string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "STATUS [%s]: %s", component, status)
|
||||
}
|
||||
}
|
||||
|
||||
// LogRequest logs incoming requests
|
||||
func (il *InfoLogger) LogRequest(method string, path string, userAgent string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "REQUEST [%s %s] User-Agent: %s", method, path, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
// LogResponse logs outgoing responses
|
||||
func (il *InfoLogger) LogResponse(method string, path string, statusCode int, duration string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "RESPONSE [%s %s] Status: %d, Duration: %s", method, path, statusCode, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// Global info logger instance
|
||||
var (
|
||||
infoLogger *InfoLogger
|
||||
infoOnce sync.Once
|
||||
)
|
||||
|
||||
// GetInfoLogger returns the global info logger instance
|
||||
func GetInfoLogger() *InfoLogger {
|
||||
infoOnce.Do(func() {
|
||||
infoLogger = NewInfoLogger()
|
||||
})
|
||||
return infoLogger
|
||||
}
|
||||
|
||||
// Info logs an info-level message using the global info logger
|
||||
func Info(format string, v ...interface{}) {
|
||||
GetInfoLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// InfoWithContext logs an info-level message with context using the global info logger
|
||||
func InfoWithContext(context string, format string, v ...interface{}) {
|
||||
GetInfoLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// InfoStartup logs application startup information using the global info logger
|
||||
func InfoStartup(component string, message string) {
|
||||
GetInfoLogger().LogStartup(component, message)
|
||||
}
|
||||
|
||||
// InfoShutdown logs application shutdown information using the global info logger
|
||||
func InfoShutdown(component string, message string) {
|
||||
GetInfoLogger().LogShutdown(component, message)
|
||||
}
|
||||
|
||||
// InfoOperation logs general operation information using the global info logger
|
||||
func InfoOperation(operation string, details string) {
|
||||
GetInfoLogger().LogOperation(operation, details)
|
||||
}
|
||||
|
||||
// InfoStatus logs status changes or updates using the global info logger
|
||||
func InfoStatus(component string, status string) {
|
||||
GetInfoLogger().LogStatus(component, status)
|
||||
}
|
||||
|
||||
// InfoRequest logs incoming requests using the global info logger
|
||||
func InfoRequest(method string, path string, userAgent string) {
|
||||
GetInfoLogger().LogRequest(method, path, userAgent)
|
||||
}
|
||||
|
||||
// InfoResponse logs outgoing responses using the global info logger
|
||||
func InfoResponse(method string, path string, statusCode int, duration string) {
|
||||
GetInfoLogger().LogResponse(method, path, statusCode, duration)
|
||||
}
|
||||
213
local/utl/logging/logger.go
Normal file
213
local/utl/logging/logger.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// Legacy logger for backward compatibility
|
||||
logger *Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Logger maintains backward compatibility with existing code
|
||||
type Logger struct {
|
||||
base *BaseLogger
|
||||
errorLogger *ErrorLogger
|
||||
warnLogger *WarnLogger
|
||||
infoLogger *InfoLogger
|
||||
debugLogger *DebugLogger
|
||||
}
|
||||
|
||||
// Initialize creates or gets the singleton logger instance
|
||||
// This maintains backward compatibility with existing code
|
||||
func Initialize() (*Logger, error) {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
logger, err = newLogger()
|
||||
})
|
||||
return logger, err
|
||||
}
|
||||
|
||||
func newLogger() (*Logger, error) {
|
||||
// Initialize the base logger
|
||||
baseLogger, err := InitializeBase("log")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the legacy logger wrapper
|
||||
logger := &Logger{
|
||||
base: baseLogger,
|
||||
errorLogger: GetErrorLogger(),
|
||||
warnLogger: GetWarnLogger(),
|
||||
infoLogger: GetInfoLogger(),
|
||||
debugLogger: GetDebugLogger(),
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// Close closes the logger
|
||||
func (l *Logger) Close() error {
|
||||
if l.base != nil {
|
||||
return l.base.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Legacy methods for backward compatibility
|
||||
func (l *Logger) log(level, format string, v ...interface{}) {
|
||||
if l.base != nil {
|
||||
l.base.LogWithCaller(LogLevel(level), 3, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Info(format string, v ...interface{}) {
|
||||
if l.infoLogger != nil {
|
||||
l.infoLogger.Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Error(format string, v ...interface{}) {
|
||||
if l.errorLogger != nil {
|
||||
l.errorLogger.Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(format string, v ...interface{}) {
|
||||
if l.warnLogger != nil {
|
||||
l.warnLogger.Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(format string, v ...interface{}) {
|
||||
if l.debugLogger != nil {
|
||||
l.debugLogger.Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Panic(format string) {
|
||||
if l.errorLogger != nil {
|
||||
l.errorLogger.LogFatal(format)
|
||||
}
|
||||
}
|
||||
|
||||
// Global convenience functions for backward compatibility
|
||||
// These are now implemented in individual logger files to avoid redeclaration
|
||||
func LegacyInfo(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Info(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetInfoLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func LegacyError(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Error(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetErrorLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func LegacyWarn(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Warn(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetWarnLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func LegacyDebug(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Debug(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetDebugLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func Panic(format string) {
|
||||
if logger != nil {
|
||||
logger.Panic(format)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetErrorLogger().LogFatal(format)
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced logging convenience functions
|
||||
// These provide direct access to specialized logging functions
|
||||
|
||||
// LogStartup logs application startup information
|
||||
func LogStartup(component string, message string) {
|
||||
GetInfoLogger().LogStartup(component, message)
|
||||
}
|
||||
|
||||
// LogShutdown logs application shutdown information
|
||||
func LogShutdown(component string, message string) {
|
||||
GetInfoLogger().LogShutdown(component, message)
|
||||
}
|
||||
|
||||
// LogOperation logs general operation information
|
||||
func LogOperation(operation string, details string) {
|
||||
GetInfoLogger().LogOperation(operation, details)
|
||||
}
|
||||
|
||||
// LogRequest logs incoming HTTP requests
|
||||
func LogRequest(method string, path string, userAgent string) {
|
||||
GetInfoLogger().LogRequest(method, path, userAgent)
|
||||
}
|
||||
|
||||
// LogResponse logs outgoing HTTP responses
|
||||
func LogResponse(method string, path string, statusCode int, duration string) {
|
||||
GetInfoLogger().LogResponse(method, path, statusCode, duration)
|
||||
}
|
||||
|
||||
// LogSQL logs SQL queries for debugging
|
||||
func LogSQL(query string, args ...interface{}) {
|
||||
GetDebugLogger().LogSQL(query, args...)
|
||||
}
|
||||
|
||||
// LogMemory logs memory usage information
|
||||
func LogMemory() {
|
||||
GetDebugLogger().LogMemory()
|
||||
}
|
||||
|
||||
// LogTiming logs timing information for performance debugging
|
||||
func LogTiming(operation string, duration interface{}) {
|
||||
GetDebugLogger().LogTiming(operation, duration)
|
||||
}
|
||||
|
||||
// GetLegacyLogger returns the legacy logger instance for backward compatibility
|
||||
func GetLegacyLogger() *Logger {
|
||||
if logger == nil {
|
||||
logger, _ = Initialize()
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
// InitializeLogging initializes all logging components
|
||||
func InitializeLogging() error {
|
||||
// Initialize legacy logger for backward compatibility
|
||||
_, err := Initialize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize legacy logger: %v", err)
|
||||
}
|
||||
|
||||
// Pre-initialize all logger types to ensure separate log files
|
||||
GetErrorLogger()
|
||||
GetWarnLogger()
|
||||
GetInfoLogger()
|
||||
GetDebugLogger()
|
||||
|
||||
// Log successful initialization
|
||||
Info("Logging system initialized successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
98
local/utl/logging/warn.go
Normal file
98
local/utl/logging/warn.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// WarnLogger handles warn-level logging
|
||||
type WarnLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewWarnLogger creates a new warn logger instance
|
||||
func NewWarnLogger() *WarnLogger {
|
||||
base, _ := InitializeBase("warn")
|
||||
return &WarnLogger{
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes a warn-level log entry
|
||||
func (wl *WarnLogger) Log(format string, v ...interface{}) {
|
||||
if wl.base != nil {
|
||||
wl.base.Log(LogLevelWarn, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes a warn-level log entry with additional context
|
||||
func (wl *WarnLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if wl.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
wl.base.Log(LogLevelWarn, contextualFormat, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogDeprecation logs a deprecation warning
|
||||
func (wl *WarnLogger) LogDeprecation(feature string, alternative string) {
|
||||
if wl.base != nil {
|
||||
if alternative != "" {
|
||||
wl.base.Log(LogLevelWarn, "DEPRECATED: %s is deprecated, use %s instead", feature, alternative)
|
||||
} else {
|
||||
wl.base.Log(LogLevelWarn, "DEPRECATED: %s is deprecated", feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogConfiguration logs configuration-related warnings
|
||||
func (wl *WarnLogger) LogConfiguration(setting string, message string) {
|
||||
if wl.base != nil {
|
||||
wl.base.Log(LogLevelWarn, "CONFIG WARNING [%s]: %s", setting, message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogPerformance logs performance-related warnings
|
||||
func (wl *WarnLogger) LogPerformance(operation string, threshold string, actual string) {
|
||||
if wl.base != nil {
|
||||
wl.base.Log(LogLevelWarn, "PERFORMANCE WARNING [%s]: exceeded threshold %s, actual: %s", operation, threshold, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Global warn logger instance
|
||||
var (
|
||||
warnLogger *WarnLogger
|
||||
warnOnce sync.Once
|
||||
)
|
||||
|
||||
// GetWarnLogger returns the global warn logger instance
|
||||
func GetWarnLogger() *WarnLogger {
|
||||
warnOnce.Do(func() {
|
||||
warnLogger = NewWarnLogger()
|
||||
})
|
||||
return warnLogger
|
||||
}
|
||||
|
||||
// Warn logs a warn-level message using the global warn logger
|
||||
func Warn(format string, v ...interface{}) {
|
||||
GetWarnLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// WarnWithContext logs a warn-level message with context using the global warn logger
|
||||
func WarnWithContext(context string, format string, v ...interface{}) {
|
||||
GetWarnLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// WarnDeprecation logs a deprecation warning using the global warn logger
|
||||
func WarnDeprecation(feature string, alternative string) {
|
||||
GetWarnLogger().LogDeprecation(feature, alternative)
|
||||
}
|
||||
|
||||
// WarnConfiguration logs configuration-related warnings using the global warn logger
|
||||
func WarnConfiguration(setting string, message string) {
|
||||
GetWarnLogger().LogConfiguration(setting, message)
|
||||
}
|
||||
|
||||
// WarnPerformance logs performance-related warnings using the global warn logger
|
||||
func WarnPerformance(operation string, threshold string, actual string) {
|
||||
GetWarnLogger().LogPerformance(operation, threshold, actual)
|
||||
}
|
||||
336
local/utl/password/password.go
Normal file
336
local/utl/password/password.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinPasswordLength minimum password length
|
||||
MinPasswordLength = 8
|
||||
// MaxPasswordLength maximum password length
|
||||
MaxPasswordLength = 128
|
||||
// DefaultCost default bcrypt cost
|
||||
DefaultCost = 12
|
||||
)
|
||||
|
||||
// HashPassword hashes a plain text password using bcrypt
|
||||
func HashPassword(password string) (string, error) {
|
||||
if err := ValidatePasswordStrength(password); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// CheckPasswordHash compares a plain text password with its hash
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ValidatePasswordStrength validates password strength requirements
|
||||
func ValidatePasswordStrength(password string) error {
|
||||
if len(password) < MinPasswordLength {
|
||||
return errors.New("password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
if len(password) > MaxPasswordLength {
|
||||
return errors.New("password must not exceed 128 characters")
|
||||
}
|
||||
|
||||
var (
|
||||
hasUpper = false
|
||||
hasLower = false
|
||||
hasNumber = false
|
||||
hasSpecial = false
|
||||
)
|
||||
|
||||
for _, char := range password {
|
||||
switch {
|
||||
case unicode.IsUpper(char):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(char):
|
||||
hasLower = true
|
||||
case unicode.IsNumber(char):
|
||||
hasNumber = true
|
||||
case unicode.IsPunct(char) || unicode.IsSymbol(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 !hasNumber {
|
||||
return errors.New("password must contain at least one digit")
|
||||
}
|
||||
|
||||
if !hasSpecial {
|
||||
return errors.New("password must contain at least one special character")
|
||||
}
|
||||
|
||||
// Check for common patterns
|
||||
if isCommonPassword(password) {
|
||||
return errors.New("password is too common, please choose a stronger password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isCommonPassword checks if password is in list of common passwords
|
||||
func isCommonPassword(password string) bool {
|
||||
commonPasswords := []string{
|
||||
"password", "123456", "password123", "admin", "qwerty",
|
||||
"letmein", "welcome", "monkey", "1234567890", "password1",
|
||||
"123456789", "welcome123", "admin123", "root", "test",
|
||||
"guest", "password12", "changeme", "default", "temp",
|
||||
}
|
||||
|
||||
for _, common := range commonPasswords {
|
||||
if password == common {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidatePasswordComplexity validates password against additional complexity rules
|
||||
func ValidatePasswordComplexity(password string) error {
|
||||
if err := ValidatePasswordStrength(password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for repeated characters (more than 3 consecutive)
|
||||
if hasRepeatedChars(password, 3) {
|
||||
return errors.New("password must not contain more than 3 consecutive identical characters")
|
||||
}
|
||||
|
||||
// Check for sequential characters (like "1234" or "abcd")
|
||||
if hasSequentialChars(password, 4) {
|
||||
return errors.New("password must not contain sequential characters")
|
||||
}
|
||||
|
||||
// Check for keyboard patterns
|
||||
if hasKeyboardPattern(password) {
|
||||
return errors.New("password must not contain keyboard patterns")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasRepeatedChars checks for repeated consecutive characters
|
||||
func hasRepeatedChars(password string, maxRepeat int) bool {
|
||||
if len(password) < maxRepeat+1 {
|
||||
return false
|
||||
}
|
||||
|
||||
count := 1
|
||||
for i := 1; i < len(password); i++ {
|
||||
if password[i] == password[i-1] {
|
||||
count++
|
||||
if count > maxRepeat {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
count = 1
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// hasSequentialChars checks for sequential characters
|
||||
func hasSequentialChars(password string, minSequence int) bool {
|
||||
if len(password) < minSequence {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i <= len(password)-minSequence; i++ {
|
||||
isSequential := true
|
||||
isReverseSequential := true
|
||||
|
||||
for j := 1; j < minSequence; j++ {
|
||||
if int(password[i+j]) != int(password[i+j-1])+1 {
|
||||
isSequential = false
|
||||
}
|
||||
if int(password[i+j]) != int(password[i+j-1])-1 {
|
||||
isReverseSequential = false
|
||||
}
|
||||
}
|
||||
|
||||
if isSequential || isReverseSequential {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// hasKeyboardPattern checks for common keyboard patterns
|
||||
func hasKeyboardPattern(password string) bool {
|
||||
keyboardPatterns := []string{
|
||||
"qwerty", "asdf", "zxcv", "qwertyuiop", "asdfghjkl", "zxcvbnm",
|
||||
"1234567890", "qazwsx", "wsxedc", "rfvtgb", "nhyujm", "iklop",
|
||||
}
|
||||
|
||||
lowerPassword := regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(password, "")
|
||||
lowerPassword = regexp.MustCompile(`[A-Z]`).ReplaceAllStringFunc(lowerPassword, func(s string) string {
|
||||
return string(rune(s[0]) + 32)
|
||||
})
|
||||
|
||||
for _, pattern := range keyboardPatterns {
|
||||
if len(lowerPassword) >= len(pattern) {
|
||||
for i := 0; i <= len(lowerPassword)-len(pattern); i++ {
|
||||
if lowerPassword[i:i+len(pattern)] == pattern {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateRandomPassword generates a random password with specified length
|
||||
func GenerateRandomPassword(length int) (string, error) {
|
||||
if length < MinPasswordLength {
|
||||
length = MinPasswordLength
|
||||
}
|
||||
if length > MaxPasswordLength {
|
||||
length = MaxPasswordLength
|
||||
}
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
|
||||
// Use crypto/rand for secure random generation
|
||||
password := make([]byte, length)
|
||||
for i := range password {
|
||||
// Simple implementation - in production, use crypto/rand
|
||||
password[i] = charset[i%len(charset)]
|
||||
}
|
||||
|
||||
// Ensure password meets complexity requirements
|
||||
result := string(password)
|
||||
if err := ValidatePasswordStrength(result); err != nil {
|
||||
// Fallback to a known good password pattern if generation fails
|
||||
return generateFallbackPassword(length), nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// generateFallbackPassword generates a password that meets all requirements
|
||||
func generateFallbackPassword(length int) string {
|
||||
if length < MinPasswordLength {
|
||||
length = MinPasswordLength
|
||||
}
|
||||
|
||||
// Start with a base that meets all requirements
|
||||
base := "Aa1!"
|
||||
remaining := length - len(base)
|
||||
|
||||
// Fill remaining with mixed characters
|
||||
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
|
||||
for i := 0; i < remaining; i++ {
|
||||
base += string(charset[i%len(charset)])
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
// GetPasswordStrengthScore returns a score from 0-100 indicating password strength
|
||||
func GetPasswordStrengthScore(password string) int {
|
||||
score := 0
|
||||
|
||||
// Length score (0-25 points)
|
||||
if len(password) >= 8 {
|
||||
score += 5
|
||||
}
|
||||
if len(password) >= 12 {
|
||||
score += 10
|
||||
}
|
||||
if len(password) >= 16 {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// Character variety (0-40 points)
|
||||
var hasUpper, hasLower, hasNumber, hasSpecial bool
|
||||
for _, char := range password {
|
||||
if unicode.IsUpper(char) {
|
||||
hasUpper = true
|
||||
} else if unicode.IsLower(char) {
|
||||
hasLower = true
|
||||
} else if unicode.IsNumber(char) {
|
||||
hasNumber = true
|
||||
} else if unicode.IsPunct(char) || unicode.IsSymbol(char) {
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasUpper {
|
||||
score += 10
|
||||
}
|
||||
if hasLower {
|
||||
score += 10
|
||||
}
|
||||
if hasNumber {
|
||||
score += 10
|
||||
}
|
||||
if hasSpecial {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// Complexity bonus (0-35 points)
|
||||
if !isCommonPassword(password) {
|
||||
score += 10
|
||||
}
|
||||
if !hasRepeatedChars(password, 2) {
|
||||
score += 10
|
||||
}
|
||||
if !hasSequentialChars(password, 3) {
|
||||
score += 10
|
||||
}
|
||||
if !hasKeyboardPattern(password) {
|
||||
score += 5
|
||||
}
|
||||
|
||||
// Cap at 100
|
||||
if score > 100 {
|
||||
score = 100
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// GetPasswordStrengthLevel returns a human-readable strength level
|
||||
func GetPasswordStrengthLevel(password string) string {
|
||||
score := GetPasswordStrengthScore(password)
|
||||
|
||||
switch {
|
||||
case score >= 80:
|
||||
return "Very Strong"
|
||||
case score >= 60:
|
||||
return "Strong"
|
||||
case score >= 40:
|
||||
return "Medium"
|
||||
case score >= 20:
|
||||
return "Weak"
|
||||
default:
|
||||
return "Very Weak"
|
||||
}
|
||||
}
|
||||
157
local/utl/server/server.go
Normal file
157
local/utl/server/server.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"omega-server/local/api"
|
||||
"omega-server/local/middleware/security"
|
||||
"omega-server/local/utl/logging"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/helmet"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/swagger"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func Start(di *dig.Container) *fiber.App {
|
||||
app := fiber.New(fiber.Config{
|
||||
AppName: "Omega Project Management",
|
||||
ServerHeader: "omega-server",
|
||||
EnablePrintRoutes: true,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
BodyLimit: 10 * 1024 * 1024, // 10MB
|
||||
Prefork: false,
|
||||
CaseSensitive: false,
|
||||
StrictRouting: false,
|
||||
DisableKeepalive: false,
|
||||
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||
// Custom error handler
|
||||
code := fiber.StatusInternalServerError
|
||||
if e, ok := err.(*fiber.Error); ok {
|
||||
code = e.Code
|
||||
}
|
||||
|
||||
// Log error
|
||||
logging.Error("HTTP Error: %v, Path: %s, Method: %s, IP: %s",
|
||||
err, c.Path(), c.Method(), c.IP())
|
||||
|
||||
// Return JSON error response
|
||||
return c.Status(code).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
"code": code,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Initialize security middleware
|
||||
securityMW := security.NewSecurityMiddleware()
|
||||
|
||||
// Add recovery middleware first
|
||||
app.Use(recover.New(recover.Config{
|
||||
EnableStackTrace: true,
|
||||
}))
|
||||
|
||||
// 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
|
||||
|
||||
// Add Helmet middleware for security headers
|
||||
app.Use(helmet.New(helmet.Config{
|
||||
XSSProtection: "1; mode=block",
|
||||
ContentTypeNosniff: "nosniff",
|
||||
XFrameOptions: "DENY",
|
||||
HSTSMaxAge: 31536000,
|
||||
HSTSPreloadEnabled: true,
|
||||
ContentSecurityPolicy: "default-src 'self'",
|
||||
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||
}))
|
||||
|
||||
// Configure CORS
|
||||
allowedOrigin := os.Getenv("CORS_ALLOWED_ORIGIN")
|
||||
if allowedOrigin == "" {
|
||||
allowedOrigin = "http://localhost:5173"
|
||||
}
|
||||
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: allowedOrigin,
|
||||
AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Requested-With",
|
||||
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS, PATCH",
|
||||
AllowCredentials: true,
|
||||
MaxAge: 86400, // 24 hours
|
||||
}))
|
||||
|
||||
// Add request logging middleware
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
start := time.Now()
|
||||
|
||||
// Process request
|
||||
err := c.Next()
|
||||
|
||||
// Log request
|
||||
duration := time.Since(start)
|
||||
logging.InfoResponse(
|
||||
c.Method(),
|
||||
c.Path(),
|
||||
c.Response().StatusCode(),
|
||||
duration.String(),
|
||||
)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
// Swagger documentation
|
||||
app.Get("/swagger/*", swagger.HandlerDefault)
|
||||
|
||||
// Health check endpoint
|
||||
app.Get("/health", func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"version": "1.0.0",
|
||||
})
|
||||
})
|
||||
|
||||
// Ping endpoint
|
||||
app.Get("/ping", func(c *fiber.Ctx) error {
|
||||
return c.SendString("pong")
|
||||
})
|
||||
|
||||
// Initialize API routes
|
||||
api.Init(di, app)
|
||||
|
||||
// 404 handler
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"error": "Route not found",
|
||||
"path": c.Path(),
|
||||
})
|
||||
})
|
||||
|
||||
// Get port from environment
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "3000" // Default port
|
||||
}
|
||||
|
||||
logging.Info("Starting server on port %s", port)
|
||||
logging.Info("Swagger documentation available at: http://localhost:%s/swagger/", port)
|
||||
logging.Info("Health check available at: http://localhost:%s/health", port)
|
||||
|
||||
// Start server
|
||||
if err := app.Listen(":" + port); err != nil {
|
||||
logging.Error("Failed to start server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
176
scripts/generate-secrets.ps1
Normal file
176
scripts/generate-secrets.ps1
Normal file
@@ -0,0 +1,176 @@
|
||||
# Bootstrap App - Secret Generation Script
|
||||
# This script generates cryptographically secure secrets for the Bootstrap App
|
||||
|
||||
Write-Host "Bootstrap App - 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 = @(
|
||||
"# Bootstrap App 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=app.db",
|
||||
"PORT=3000",
|
||||
"CORS_ALLOWED_ORIGIN=http://localhost:5173",
|
||||
"DEFAULT_ADMIN_PASSWORD=change-this-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 characters as required for AES-256" -ForegroundColor Yellow
|
||||
Write-Host "5. Change the DEFAULT_ADMIN_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. Build and run the application: go run cmd/api/main.go" -ForegroundColor White
|
||||
Write-Host "3. Change the default admin password on first login" -ForegroundColor White
|
||||
Write-Host "4. Access the API documentation at: http://localhost:3000/swagger/" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Happy coding! 🚀" -ForegroundColor Green
|
||||
163
scripts/generate-secrets.sh
Normal file
163
scripts/generate-secrets.sh
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Bootstrap App - Secret Generation Script
|
||||
# This script generates cryptographically secure secrets for the Bootstrap App
|
||||
|
||||
echo "Bootstrap App - Secret Generation Script"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
WHITE='\033[1;37m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to generate hex string
|
||||
generate_hex_string() {
|
||||
local length=$1
|
||||
openssl rand -hex $length 2>/dev/null || xxd -l $length -p /dev/urandom | tr -d '\n'
|
||||
}
|
||||
|
||||
# Function to generate base64 string
|
||||
generate_base64_string() {
|
||||
local length=$1
|
||||
openssl rand -base64 $length 2>/dev/null || dd if=/dev/urandom bs=$length count=1 2>/dev/null | base64 | tr -d '\n'
|
||||
}
|
||||
|
||||
# Check if required tools are available
|
||||
check_dependencies() {
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
if ! command -v xxd >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: Neither openssl nor xxd is available. Please install one of them.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate secrets
|
||||
echo -e "${YELLOW}Generating cryptographically secure secrets...${NC}"
|
||||
echo ""
|
||||
|
||||
check_dependencies
|
||||
|
||||
JWT_SECRET=$(generate_base64_string 64)
|
||||
APP_SECRET=$(generate_hex_string 32)
|
||||
APP_SECRET_CODE=$(generate_hex_string 32)
|
||||
ENCRYPTION_KEY=$(generate_hex_string 16)
|
||||
|
||||
# Display generated secrets
|
||||
echo -e "${CYAN}Generated Secrets:${NC}"
|
||||
echo -e "${CYAN}==================${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}JWT_SECRET=${NC}${YELLOW}$JWT_SECRET${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}APP_SECRET=${NC}${YELLOW}$APP_SECRET${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}APP_SECRET_CODE=${NC}${YELLOW}$APP_SECRET_CODE${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}ENCRYPTION_KEY=${NC}${YELLOW}$ENCRYPTION_KEY${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if .env file exists
|
||||
ENV_FILE=".env"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo -e "${RED}Warning: .env file already exists!${NC}"
|
||||
read -p "Do you want to update it with new secrets? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
UPDATE_FILE=true
|
||||
else
|
||||
UPDATE_FILE=false
|
||||
echo -e "${YELLOW}Secrets generated but not written to file.${NC}"
|
||||
fi
|
||||
else
|
||||
read -p "Create .env file with these secrets? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
UPDATE_FILE=false
|
||||
echo -e "${YELLOW}Secrets generated but not written to file.${NC}"
|
||||
else
|
||||
UPDATE_FILE=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$UPDATE_FILE" = true ]; then
|
||||
# Create or update .env file
|
||||
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 "${GREEN}Backed up existing .env to $BACKUP_FILE${NC}"
|
||||
|
||||
# Update secrets in existing file
|
||||
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 "${GREEN}Updated .env file with new secrets${NC}"
|
||||
else
|
||||
# Create new .env file from template
|
||||
if [ -f ".env.example" ]; then
|
||||
cp ".env.example" "$ENV_FILE"
|
||||
|
||||
# Replace placeholder values with generated secrets
|
||||
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 "${GREEN}Created .env file from template with generated secrets${NC}"
|
||||
else
|
||||
# Create minimal .env file
|
||||
cat > "$ENV_FILE" << EOF
|
||||
# Bootstrap App 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=app.db
|
||||
PORT=3000
|
||||
CORS_ALLOWED_ORIGIN=http://localhost:5173
|
||||
DEFAULT_ADMIN_PASSWORD=change-this-password
|
||||
EOF
|
||||
echo -e "${GREEN}Created minimal .env file with generated secrets${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${RED}Security Notes:${NC}"
|
||||
echo -e "${RED}===============${NC}"
|
||||
echo -e "${YELLOW}1. Keep these secrets secure and never commit them to version control${NC}"
|
||||
echo -e "${YELLOW}2. Use different secrets for each environment (dev, staging, production)${NC}"
|
||||
echo -e "${YELLOW}3. Rotate secrets regularly in production environments${NC}"
|
||||
echo -e "${YELLOW}4. The ENCRYPTION_KEY is exactly 32 characters as required for AES-256${NC}"
|
||||
echo -e "${YELLOW}5. Change the DEFAULT_ADMIN_PASSWORD immediately after first login${NC}"
|
||||
echo ""
|
||||
|
||||
# Verify encryption key length
|
||||
if [ ${#ENCRYPTION_KEY} -eq 32 ]; then
|
||||
echo -e "${GREEN}✓ Encryption key length verified (32 characters = 32 bytes for AES-256)${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Warning: Encryption key length is incorrect! Got ${#ENCRYPTION_KEY} chars, expected 32${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}Next steps:${NC}"
|
||||
echo -e "${WHITE}1. Review and customize the .env file if needed${NC}"
|
||||
echo -e "${WHITE}2. Build and run the application: go run cmd/api/main.go${NC}"
|
||||
echo -e "${WHITE}3. Change the default admin password on first login${NC}"
|
||||
echo -e "${WHITE}4. Access the API documentation at: http://localhost:3000/swagger/${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}Happy coding! 🚀${NC}"
|
||||
Reference in New Issue
Block a user