init bootstrap

This commit is contained in:
Fran Jurmanović
2025-07-06 15:02:09 +02:00
commit 016728532c
47 changed files with 8894 additions and 0 deletions

226
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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")
}
}

View 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
View 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")
}

View 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)
}

View 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()
}

View 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
View 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
View 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))
}
}

View 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
View 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
View 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
}

View 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
}

View 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
View 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
View 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)
}

View 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
}

View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
}

View 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
View 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}"