├── .dockerignore ├── .env.example ├── .env.template ├── DOCKER_DEPLOYMENT.md ├── DOCKER_QUICK_START.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_DOCKER.md ├── RESPONSIVE_PLAN.md ├── RentalCore.sql ├── cmd └── compliance │ └── main.go ├── config.json.example ├── cookies.txt ├── create-production-user.sh ├── database ├── SCHEMA.md ├── rentalcore_setup.sql ├── test_setup.sh └── validate_setup.sql ├── deploy-production.sh ├── docker-compose.example.yml ├── docker-compose.prod.yml ├── docs ├── ADMIN_GUIDE.md ├── API.md ├── ARCHITECTURE.md ├── CONFIGURATION.md ├── DATABASE_SETUP.md ├── DOCKER_DEPLOYMENT.md ├── DOCKER_QUICK_START.md ├── SECURITY.md ├── TROUBLESHOOTING.md └── USER_GUIDE.md ├── go.mod ├── go.sum ├── img ├── cables-management.png ├── case-devices-modal.png ├── cases-management.png ├── dashboard-overview.png ├── device-tree-view.png ├── devices-management.png ├── job-overview-barcode-scanner.png ├── job-scanning-interface.png ├── job-selection-scanning.png ├── login-page.png └── products-management.png ├── internal ├── compliance │ ├── audit_logger.go │ ├── digital_signature.go │ ├── gdpr_compliance.go │ ├── gobd_compliance.go │ ├── middleware.go │ └── retention_manager.go ├── config │ ├── config.go │ └── database_config.go ├── database │ └── migrations │ │ └── 001_performance_indexes.sql ├── handlers │ ├── analytics_handler.go │ ├── auth_handler.go │ ├── barcode_handler.go │ ├── cable_handler.go │ ├── case_handler.go │ ├── company_handler.go │ ├── customer_handler.go │ ├── device_handler.go │ ├── document_handler.go │ ├── equipment_package_handler.go │ ├── error_handler.go │ ├── financial_handler.go │ ├── home_handler.go │ ├── invoice_handler.go │ ├── invoice_template_handler.go │ ├── job_attachment_handler.go │ ├── job_handler.go │ ├── monitoring_handler.go │ ├── product_handler.go │ ├── profile_handler.go │ ├── pwa_handler.go │ ├── rental_equipment_handler.go │ ├── scanboard_handler.go │ ├── scanner_handler.go │ ├── search_handler.go │ ├── security_handler.go │ ├── status_handler.go │ ├── types.go │ ├── webauthn_handler.go │ └── workflow_handler.go ├── logger │ └── structured_logger.go ├── middleware │ └── performance.go ├── models │ ├── enhanced_models.go │ ├── invoice_models.go │ └── models.go ├── monitoring │ └── error_tracker.go ├── repository │ ├── cable_repository.go │ ├── case_repository.go │ ├── customer_repository.go │ ├── database.go │ ├── device_repository.go │ ├── equipment_package_repository.go │ ├── invoice_repository.go │ ├── job_attachment_repository.go │ ├── job_category_repository.go │ ├── job_repository.go │ ├── job_repository_extension.go │ ├── product_repository.go │ ├── rental_equipment_repository.go │ └── status_repository.go ├── routes │ └── scan_fallback.go ├── scan │ └── decode.go └── services │ ├── barcode_service.go │ ├── email_service.go │ └── pdf_service.go ├── jobscanner.service ├── migrations ├── 001_initial_schema.sql ├── 002_enhancement_features.sql ├── 003_package_device_enhancement.sql ├── 005_invoice_system.sql ├── 006_performance_indexes.sql ├── 007_equipment_packages.sql ├── 008_company_settings.sql ├── 009_company_settings_german_fields.sql ├── 010_fix_sessions_foreign_key.sql ├── 011_add_email_settings.sql ├── 012_fix_user_preferences_fk.sql ├── 013_add_2fa_and_passkey_tables.sql ├── 014_add_missing_webauthn_table.sql ├── 021_create_rental_equipment_tables_corrected.down.sql ├── 021_create_rental_equipment_tables_corrected.up.sql ├── 022_fix_company_settings_datetime.sql ├── 023_create_job_attachments_table.down.sql ├── 023_create_job_attachments_table.up.sql ├── 024_add_pack_workflow.down.sql ├── 024_add_pack_workflow.up.sql └── compliance_tables.sql ├── restart-dev.sh ├── start-production.sh ├── start.sh └── web ├── scanner ├── decoder │ ├── decoder.go │ ├── dedupe.go │ ├── roi.go │ └── types.go ├── ui │ ├── ScannerView.tsx │ ├── camera.js │ ├── capabilities.js │ ├── gestures.js │ ├── scanner-integration.js │ └── scanner.css ├── wasm │ ├── Makefile │ ├── build.sh │ ├── decoder.wasm │ └── wasm_exec.js └── worker │ ├── decoder-manager.js │ └── decoder.worker.js ├── static ├── css │ └── rental-core-design.css ├── images │ └── icon-180.png ├── js │ ├── rental-core-design.js │ └── security-roles.js ├── manifest.json ├── scanner │ ├── ui │ │ ├── ScannerView.tsx │ │ ├── camera.js │ │ ├── capabilities.js │ │ ├── gestures.js │ │ ├── scanner-integration.js │ │ └── scanner.css │ ├── wasm │ │ ├── decoder.wasm │ │ └── wasm_exec.js │ └── worker │ │ ├── decoder-manager.js │ │ └── decoder.worker.js └── sw.js └── templates ├── analytics_dashboard.html ├── analytics_dashboard_new.html ├── base.html ├── bulk_operations.html ├── cable_form.html ├── cables_standalone.html ├── case_detail.html ├── case_device_mapping.html ├── case_form.html ├── cases_list.html ├── company_settings.html ├── customer_detail.html ├── customer_form.html ├── customers.html ├── device_detail.html ├── device_form.html ├── devices_standalone.html ├── document_upload_form.html ├── documents_list.html ├── equipment_package_detail.html ├── equipment_package_form.html ├── equipment_packages_standalone.html ├── error.html ├── error_page.html ├── error_standalone.html ├── financial_dashboard.html ├── financial_reports.html ├── home.html ├── invoice_detail.html ├── invoice_form.html ├── invoice_preview.html ├── invoice_settings_form.html ├── invoice_template_designer.html ├── invoice_template_designer_basic.html ├── invoice_template_german.html ├── invoice_templates_list.html ├── invoices_content.html ├── invoices_list.html ├── job_detail.html ├── job_form.html ├── jobs.html ├── login.html ├── login_2fa.html ├── mobile_scanner.html ├── mobile_scanner_optimized.html ├── monitoring_dashboard.html ├── navbar.html ├── products_standalone.html ├── professional_scanner.html ├── profile_settings_standalone.html ├── profile_settings_standalone_backup.html ├── rental_equipment_analytics_standalone.html ├── rental_equipment_standalone.html ├── scan_board.html ├── scan_job.html ├── scan_select_job.html ├── scanner_demo.html ├── search_results.html ├── security_audit.html ├── security_roles_standalone.html ├── signature_form.html ├── transaction_detail.html ├── transaction_form.html ├── transactions_list.html ├── user_detail.html ├── user_form.html └── users_list.html /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git files 2 | .git 3 | .gitignore 4 | 5 | # Docker files 6 | Dockerfile 7 | docker-compose.yml 8 | .dockerignore 9 | 10 | # Environment files 11 | .env 12 | 13 | # Development files 14 | *.md 15 | README* 16 | .vscode/ 17 | .idea/ 18 | 19 | # Logs and temporary files 20 | logs/*.log 21 | *.log 22 | server_output*.log 23 | server_debug.log 24 | 25 | # Build artifacts 26 | server 27 | bin/ 28 | main 29 | 30 | # Temporary and cache files 31 | uploads/ 32 | temp/ 33 | tmp/ 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Documentation 38 | docs/ 39 | *.md 40 | 41 | # Scripts 42 | *.sh 43 | deploy-*.sh 44 | start-*.sh 45 | 46 | # Development tools 47 | Makefile 48 | 49 | # Testing files 50 | *_test.go 51 | test/ 52 | tests/ 53 | 54 | # Backup files 55 | *.backup 56 | *.bak 57 | backups/ 58 | 59 | # IDE files 60 | .vscode/ 61 | .idea/ 62 | *.swp 63 | *.swo 64 | 65 | # OS generated files 66 | .DS_Store 67 | .DS_Store? 68 | ._* 69 | .Spotlight-V100 70 | .Trashes 71 | ehthumbs.db 72 | Thumbs.db -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ============================================================================= 2 | # Go Barcode Webapp - Environment Configuration Template 3 | # ============================================================================= 4 | # Copy this file to .env and customize the values for your environment 5 | 6 | # ============================================================================= 7 | # DATABASE CONFIGURATION 8 | # ============================================================================= 9 | # Database connection settings 10 | DB_HOST=your-database-host.com 11 | DB_PORT=3306 12 | DB_NAME=your-database-name 13 | DB_USERNAME=your-database-user 14 | DB_PASSWORD=your-database-password 15 | 16 | # ============================================================================= 17 | # SERVER CONFIGURATION 18 | # ============================================================================= 19 | # Application server settings 20 | APP_PORT=8080 21 | SERVER_HOST=0.0.0.0 22 | SERVER_PORT=8080 23 | 24 | # Application mode (release for production, debug for development) 25 | GIN_MODE=release 26 | 27 | # ============================================================================= 28 | # SECURITY CONFIGURATION 29 | # ============================================================================= 30 | # Encryption key for secure operations (CHANGE IN PRODUCTION!) 31 | ENCRYPTION_KEY=change-this-to-a-secure-random-key-in-production 32 | 33 | # Session timeout in seconds (default: 1 hour) 34 | SESSION_TIMEOUT=3600 35 | 36 | # ============================================================================= 37 | # EMAIL CONFIGURATION (OPTIONAL) 38 | # ============================================================================= 39 | # SMTP settings for email notifications 40 | SMTP_HOST=your-smtp-host.com 41 | SMTP_PORT=587 42 | SMTP_USERNAME=your-smtp-username 43 | SMTP_PASSWORD=your-smtp-password 44 | FROM_EMAIL=noreply@yourcompany.com 45 | FROM_NAME=Your Company Name 46 | USE_TLS=true 47 | 48 | # ============================================================================= 49 | # INVOICE CONFIGURATION 50 | # ============================================================================= 51 | # Default tax rate (adjust for your country) 52 | DEFAULT_TAX_RATE=19.0 53 | 54 | # Default payment terms in days 55 | DEFAULT_PAYMENT_TERMS=30 56 | 57 | # Currency settings 58 | CURRENCY_SYMBOL=€ 59 | CURRENCY_CODE=EUR 60 | 61 | # ============================================================================= 62 | # LOGGING CONFIGURATION 63 | # ============================================================================= 64 | # Log level: debug, info, warn, error 65 | LOG_LEVEL=info 66 | 67 | # Log file path 68 | LOG_FILE=logs/app.log 69 | 70 | # ============================================================================= 71 | # BACKUP CONFIGURATION (OPTIONAL) 72 | # ============================================================================= 73 | # Enable/disable automatic backups 74 | BACKUP_ENABLED=true 75 | 76 | # Backup interval in seconds (86400 = 24 hours) 77 | BACKUP_INTERVAL=86400 78 | 79 | # Backup retention in days 80 | BACKUP_RETENTION_DAYS=30 -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # RentalCore Environment Configuration Template 2 | # Copy this file to .env and customize the values for your environment 3 | 4 | # ================================================================= 5 | # APPLICATION CONFIGURATION 6 | # ================================================================= 7 | APP_PORT=8080 8 | GIN_MODE=release 9 | 10 | # Server Configuration 11 | SERVER_HOST=0.0.0.0 12 | SERVER_PORT=8080 13 | 14 | # ================================================================= 15 | # DATABASE CONFIGURATION 16 | # ================================================================= 17 | # External MySQL Database Settings 18 | DB_HOST=your-mysql-host.com 19 | DB_PORT=3306 20 | DB_NAME=your-database-name 21 | DB_USERNAME=your-db-username 22 | DB_PASSWORD=your-secure-db-password 23 | 24 | # Example for cloud databases: 25 | # DB_HOST=mysql-db.amazonaws.com 26 | # DB_HOST=your-server.tsunami-events.de 27 | 28 | # ================================================================= 29 | # SECURITY CONFIGURATION 30 | # ================================================================= 31 | # Generate a secure 32-character encryption key 32 | # You can generate one with: openssl rand -hex 16 33 | ENCRYPTION_KEY=your-32-character-encryption-key-here 34 | 35 | # Session timeout in seconds (default: 1 hour) 36 | SESSION_TIMEOUT=3600 37 | 38 | # ================================================================= 39 | # EMAIL CONFIGURATION (Optional) 40 | # ================================================================= 41 | # SMTP settings for sending emails (invoices, notifications, etc.) 42 | SMTP_HOST=smtp.gmail.com 43 | SMTP_PORT=587 44 | SMTP_USERNAME=your-email@gmail.com 45 | SMTP_PASSWORD=your-app-password 46 | FROM_EMAIL=noreply@yourdomain.com 47 | FROM_NAME=RentalCore System 48 | USE_TLS=true 49 | 50 | # Leave empty to disable email functionality: 51 | # SMTP_HOST= 52 | # SMTP_USERNAME= 53 | # SMTP_PASSWORD= 54 | 55 | # ================================================================= 56 | # INVOICE CONFIGURATION 57 | # ================================================================= 58 | # Default invoice settings 59 | DEFAULT_TAX_RATE=19.0 60 | DEFAULT_PAYMENT_TERMS=30 61 | CURRENCY_SYMBOL=€ 62 | CURRENCY_CODE=EUR 63 | 64 | # For different regions: 65 | # US: CURRENCY_SYMBOL=$, CURRENCY_CODE=USD, DEFAULT_TAX_RATE=8.5 66 | # UK: CURRENCY_SYMBOL=£, CURRENCY_CODE=GBP, DEFAULT_TAX_RATE=20.0 67 | 68 | # ================================================================= 69 | # LOGGING CONFIGURATION 70 | # ================================================================= 71 | LOG_LEVEL=info 72 | LOG_FILE=logs/app.log 73 | 74 | # Available log levels: debug, info, warn, error 75 | # For production, use 'info' or 'warn' 76 | # For development/debugging, use 'debug' 77 | 78 | # ================================================================= 79 | # EXAMPLE CONFIGURATIONS FOR DIFFERENT ENVIRONMENTS 80 | # ================================================================= 81 | 82 | # --- DEVELOPMENT EXAMPLE --- 83 | # APP_PORT=8080 84 | # GIN_MODE=debug 85 | # LOG_LEVEL=debug 86 | # DB_HOST=localhost 87 | # ENCRYPTION_KEY=dev-key-32-chars-long-for-testing 88 | 89 | # --- PRODUCTION EXAMPLE --- 90 | # APP_PORT=8080 91 | # GIN_MODE=release 92 | # LOG_LEVEL=info 93 | # DB_HOST=prod-mysql.yourdomain.com 94 | # ENCRYPTION_KEY=prod-secure-32-char-key-generated-securely 95 | 96 | # ================================================================= 97 | # SECURITY NOTES 98 | # ================================================================= 99 | # 1. NEVER commit your actual .env file to version control 100 | # 2. Use strong, unique passwords for database access 101 | # 3. Generate a unique ENCRYPTION_KEY for each deployment 102 | # 4. Use HTTPS in production (configure via reverse proxy) 103 | # 5. Regularly rotate passwords and keys -------------------------------------------------------------------------------- /DOCKER_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # 🐳 RentalCore Docker Hub Deployment Guide 2 | 3 | This guide explains how to build, push to Docker Hub, and deploy RentalCore on any system using Docker. 4 | 5 | ## 📋 Table of Contents 6 | 1. [Prerequisites](#prerequisites) 7 | 2. [Building and Pushing to Docker Hub](#building-and-pushing-to-docker-hub) 8 | 3. [Deploying from Docker Hub](#deploying-from-docker-hub) 9 | 4. [Configuration](#configuration) 10 | 5. [Production Deployment](#production-deployment) 11 | 6. [Troubleshooting](#troubleshooting) 12 | 13 | --- 14 | 15 | ## 🔧 Prerequisites 16 | 17 | ### For Building & Pushing: 18 | - Docker installed and running 19 | - Docker Hub account 20 | - Access to this source code 21 | 22 | ### For Deployment Only: 23 | - Docker and Docker Compose installed 24 | - Access to your MySQL database 25 | - The deployment files (docker-compose.prod.yml and .env) 26 | 27 | --- 28 | 29 | ## 🏗️ Building and Pushing to Docker Hub 30 | 31 | ### Step 1: Login to Docker Hub 32 | ```bash 33 | docker login 34 | ``` 35 | Enter your Docker Hub username and password. 36 | 37 | ### Step 2: Build the Image 38 | Replace `nbt4` with your actual Docker Hub username: 39 | 40 | ```bash 41 | # Build the image 42 | docker build -t nbt4/rentalcore:latest . 43 | 44 | # Optional: Tag with version number 45 | docker build -t nbt4/rentalcore:v1.0.0 . 46 | ``` 47 | 48 | ### Step 3: Push to Docker Hub 49 | ```bash 50 | # Push latest tag 51 | docker push nbt4/rentalcore:latest 52 | 53 | # Push version tag (if created) 54 | docker push nbt4/rentalcore:v1.0.0 55 | ``` 56 | 57 | ### Step 4: Verify Upload 58 | Visit `https://hub.docker.com/r/nbt4/rentalcore` to confirm your image is uploaded. 59 | 60 | --- 61 | 62 | ## 🚀 Deploying from Docker Hub 63 | 64 | ### Step 1: Download Deployment Files 65 | On your target server, create a new directory and download these files: 66 | - `docker-compose.prod.yml` 67 | - `.env.template` 68 | 69 | ```bash 70 | mkdir rentalcore-deployment 71 | cd rentalcore-deployment 72 | 73 | # Download files (replace with your actual URLs) 74 | wget https://raw.githubusercontent.com/nbt4/rentalcore/main/docker-compose.prod.yml 75 | wget https://raw.githubusercontent.com/nbt4/rentalcore/main/.env.template 76 | ``` 77 | 78 | ### Step 2: Configure Environment 79 | ```bash 80 | # Copy template to create your configuration 81 | cp .env.template .env 82 | 83 | # Edit the configuration 84 | nano .env 85 | ``` 86 | 87 | **Important**: Update the Docker image name in `docker-compose.prod.yml`: 88 | ```yaml 89 | services: 90 | rentalcore: 91 | image: nbt4/rentalcore:latest 92 | ``` 93 | 94 | ### Step 3: Configure Your Database 95 | Edit `.env` file with your database settings: 96 | ```env 97 | # Database Configuration 98 | DB_HOST=your-mysql-host.com 99 | DB_PORT=3306 100 | DB_NAME=your-database-name 101 | DB_USERNAME=your-db-username 102 | DB_PASSWORD=your-secure-password 103 | 104 | # Security (REQUIRED) 105 | ENCRYPTION_KEY=your-32-character-encryption-key-here 106 | ``` 107 | 108 | ### Step 4: Deploy 109 | ```bash 110 | # Start the application 111 | docker-compose -f docker-compose.prod.yml up -d 112 | 113 | # Check status 114 | docker-compose -f docker-compose.prod.yml ps 115 | 116 | # View logs 117 | docker-compose -f docker-compose.prod.yml logs -f rentalcore 118 | ``` 119 | 120 | --- 121 | 122 | ## ⚙️ Configuration 123 | 124 | ### Required Environment Variables 125 | ```env 126 | # Database (Required) 127 | DB_HOST=your-database-host 128 | DB_NAME=your-database-name 129 | DB_USERNAME=your-db-user 130 | DB_PASSWORD=your-db-password 131 | 132 | # Security (Required) 133 | ENCRYPTION_KEY=generate-a-32-character-key 134 | ``` 135 | 136 | ### Optional Environment Variables 137 | ```env 138 | # Application 139 | APP_PORT=8080 140 | GIN_MODE=release 141 | 142 | # Email (for notifications and invoices) 143 | SMTP_HOST=smtp.gmail.com 144 | SMTP_PORT=587 145 | SMTP_USERNAME=your-email@gmail.com 146 | SMTP_PASSWORD=your-app-password 147 | 148 | # Invoice Settings 149 | DEFAULT_TAX_RATE=19.0 150 | CURRENCY_SYMBOL=€ 151 | CURRENCY_CODE=EUR 152 | ``` 153 | 154 | ### Generating Encryption Key 155 | ```bash 156 | # Generate a secure 32-character key 157 | openssl rand -hex 16 158 | ``` 159 | 160 | --- 161 | 162 | ## 🏭 Production Deployment 163 | 164 | ### With Reverse Proxy (Recommended) 165 | Create a reverse proxy setup with nginx or Traefik: 166 | 167 | **nginx example:** 168 | ```nginx 169 | server { 170 | listen 80; 171 | server_name rentalcore.yourdomain.com; 172 | 173 | location / { 174 | proxy_pass http://localhost:8080; 175 | proxy_set_header Host $host; 176 | proxy_set_header X-Real-IP $remote_addr; 177 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 178 | proxy_set_header X-Forwarded-Proto $scheme; 179 | } 180 | } 181 | ``` 182 | 183 | ### SSL/HTTPS Setup 184 | Use Let's Encrypt with certbot: 185 | ```bash 186 | sudo certbot --nginx -d rentalcore.yourdomain.com 187 | ``` 188 | 189 | ### Backup Volumes 190 | Your data is stored in Docker volumes. To backup: 191 | ```bash 192 | # Create backup directory 193 | mkdir backups 194 | 195 | # Backup uploads 196 | docker run --rm -v rentalcore-deployment_rentalcore_uploads:/data -v $(pwd)/backups:/backup alpine tar czf /backup/uploads.tar.gz -C /data . 197 | 198 | # Backup logs 199 | docker run --rm -v rentalcore-deployment_rentalcore_logs:/data -v $(pwd)/backups:/backup alpine tar czf /backup/logs.tar.gz -C /data . 200 | ``` 201 | 202 | --- 203 | 204 | ## 🔍 Troubleshooting 205 | 206 | ### Health Check 207 | Test if the application is running: 208 | ```bash 209 | curl http://localhost:8080/health 210 | # Should return: {"service":"RentalCore","status":"ok"} 211 | ``` 212 | 213 | ### Database Connection Issues 214 | ```bash 215 | # Test database connectivity 216 | docker-compose -f docker-compose.prod.yml exec rentalcore nc -z $DB_HOST $DB_PORT 217 | ``` 218 | 219 | ### View Application Logs 220 | ```bash 221 | # Real-time logs 222 | docker-compose -f docker-compose.prod.yml logs -f rentalcore 223 | 224 | # Last 100 lines 225 | docker-compose -f docker-compose.prod.yml logs --tail=100 rentalcore 226 | ``` 227 | 228 | ### Common Issues 229 | 230 | **1. Database Connection Failed** 231 | - Check DB credentials in `.env` 232 | - Ensure database server is accessible from Docker container 233 | - Verify firewall settings 234 | 235 | **2. Permission Denied** 236 | - Check file permissions on volumes 237 | - Ensure Docker has permission to access volumes 238 | 239 | **3. Out of Memory** 240 | - Increase Docker memory limits 241 | - Monitor resource usage: `docker stats` 242 | 243 | ### Restart Services 244 | ```bash 245 | # Restart application only 246 | docker-compose -f docker-compose.prod.yml restart rentalcore 247 | 248 | # Restart all services 249 | docker-compose -f docker-compose.prod.yml restart 250 | 251 | # Full stop and start 252 | docker-compose -f docker-compose.prod.yml down 253 | docker-compose -f docker-compose.prod.yml up -d 254 | ``` 255 | 256 | --- 257 | 258 | ## 📁 File Structure for Deployment 259 | 260 | Your deployment directory should look like this: 261 | ``` 262 | rentalcore-deployment/ 263 | ├── docker-compose.prod.yml 264 | ├── .env 265 | ├── .env.template 266 | └── backups/ (optional) 267 | ``` 268 | 269 | --- 270 | 271 | ## 🔄 Updates 272 | 273 | To update to a new version: 274 | ```bash 275 | # Pull the latest image 276 | docker-compose -f docker-compose.prod.yml pull 277 | 278 | # Restart with new image 279 | docker-compose -f docker-compose.prod.yml up -d 280 | ``` 281 | 282 | --- 283 | 284 | ## 📞 Support 285 | 286 | - **Documentation**: Check this file for common issues 287 | - **Logs**: Always check application logs first 288 | - **Health Check**: Use `/health` endpoint to verify service status 289 | - **Database**: Verify database connectivity separately 290 | 291 | **Remember**: Always backup your data before updates! -------------------------------------------------------------------------------- /DOCKER_QUICK_START.md: -------------------------------------------------------------------------------- 1 | # 🚀 RentalCore - Docker Quick Start 2 | 3 | Deploy RentalCore in 5 minutes using Docker Hub! 4 | 5 | ## 📦 For Deployment (End Users) 6 | 7 | ### 1. Create Deployment Directory 8 | ```bash 9 | mkdir rentalcore-app && cd rentalcore-app 10 | ``` 11 | 12 | ### 2. Download Required Files 13 | ```bash 14 | # Download docker-compose file 15 | curl -O https://raw.githubusercontent.com/nbt4/rentalcore/main/docker-compose.prod.yml 16 | 17 | # Download environment template 18 | curl -O https://raw.githubusercontent.com/nbt4/rentalcore/main/.env.template 19 | ``` 20 | 21 | ### 3. Configure Environment 22 | ```bash 23 | # Create your configuration 24 | cp .env.template .env 25 | 26 | # Edit with your settings 27 | nano .env 28 | ``` 29 | 30 | **Minimum required settings:** 31 | ```env 32 | # Database 33 | DB_HOST=your-mysql-host.com 34 | DB_NAME=your_database_name 35 | DB_USERNAME=your_db_user 36 | DB_PASSWORD=your_secure_password 37 | 38 | # Security (generate with: openssl rand -hex 16) 39 | ENCRYPTION_KEY=your-32-character-encryption-key 40 | ``` 41 | 42 | ### 4. Update Docker Image 43 | Edit `docker-compose.prod.yml` and update the image name: 44 | ```yaml 45 | services: 46 | rentalcore: 47 | image: nbt4/rentalcore:latest 48 | ``` 49 | 50 | ### 5. Deploy 51 | ```bash 52 | docker-compose -f docker-compose.prod.yml up -d 53 | ``` 54 | 55 | ### 6. Access Application 56 | - Open: `http://your-server:8080` 57 | - Health check: `http://your-server:8080/health` 58 | 59 | --- 60 | 61 | ## 🛠️ For Developers (Building & Publishing) 62 | 63 | ### 1. Set Your Docker Hub Username 64 | ```bash 65 | ./build-and-push.sh --set-username nbt4 66 | ``` 67 | 68 | ### 2. Login to Docker Hub 69 | ```bash 70 | docker login 71 | ``` 72 | 73 | ### 3. Build and Push 74 | ```bash 75 | ./build-and-push.sh 76 | ``` 77 | 78 | This will: 79 | - ✅ Build the Docker image 80 | - ✅ Tag with `latest` and timestamp 81 | - ✅ Push to Docker Hub 82 | - ✅ Make it available for deployment anywhere 83 | 84 | --- 85 | 86 | ## 📋 Requirements 87 | 88 | **For Deployment:** 89 | - Docker & Docker Compose 90 | - MySQL database (local or cloud) 91 | 92 | **For Building:** 93 | - Docker 94 | - Docker Hub account 95 | - Source code access 96 | 97 | --- 98 | 99 | ## 🔗 Links 100 | 101 | - **Full Documentation**: [DOCKER_DEPLOYMENT.md](DOCKER_DEPLOYMENT.md) 102 | - **Environment Template**: [.env.template](.env.template) 103 | - **Production Compose**: [docker-compose.prod.yml](docker-compose.prod.yml) 104 | 105 | --- 106 | 107 | ## ⚡ One-Liner Deployment 108 | 109 | ```bash 110 | mkdir rentalcore && cd rentalcore && curl -O https://raw.githubusercontent.com/nbt4/rentalcore/main/docker-compose.prod.yml && curl -O https://raw.githubusercontent.com/nbt4/rentalcore/main/.env.template && cp .env.template .env && echo "Edit .env file with your settings, then run: docker-compose -f docker-compose.prod.yml up -d" 111 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.23-alpine AS builder 3 | 4 | # Install build dependencies 5 | RUN apk add --no-cache git 6 | 7 | # Set working directory 8 | WORKDIR /app 9 | 10 | # Copy go mod files 11 | COPY go.mod go.sum ./ 12 | 13 | # Download dependencies 14 | RUN go mod download 15 | 16 | # Copy source code 17 | COPY . . 18 | 19 | # Build the WASM decoder 20 | RUN cd web/scanner/wasm && \ 21 | chmod +x build.sh && \ 22 | GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o decoder.wasm ../decoder && \ 23 | cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" ./wasm_exec.js 2>/dev/null || \ 24 | cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./wasm_exec.js && \ 25 | echo "WASM decoder built successfully" 26 | 27 | # Build the application 28 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server cmd/server/main.go 29 | 30 | # Production stage 31 | FROM alpine:latest 32 | 33 | # Install ca-certificates for HTTPS requests 34 | RUN apk --no-cache add ca-certificates tzdata 35 | 36 | # Create app directory 37 | WORKDIR /app 38 | 39 | # Create non-root user 40 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 41 | 42 | # Copy binary from builder stage 43 | COPY --from=builder /app/server . 44 | 45 | # Copy web assets 46 | COPY --chown=appuser:appgroup web/ web/ 47 | COPY --chown=appuser:appgroup migrations/ migrations/ 48 | COPY --chown=appuser:appgroup keys/ keys/ 49 | 50 | # Create directories for uploads and logs 51 | RUN mkdir -p uploads logs archives && \ 52 | chown -R appuser:appgroup uploads logs archives 53 | 54 | # Switch to non-root user 55 | USER appuser 56 | 57 | # Expose port 58 | EXPOSE 8080 59 | 60 | # Health check 61 | HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ 62 | CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 63 | 64 | # Run the application 65 | CMD ["./server"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 RentalCore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # TS Jobscanner Makefile 2 | 3 | .PHONY: build run clean user-manager help 4 | 5 | # Default target 6 | all: build user-manager 7 | 8 | # Build the main server 9 | build: 10 | @echo "Building TS Jobscanner server..." 11 | go build -o server cmd/server/main.go 12 | 13 | # Build the user management tool 14 | user-manager: 15 | @echo "Building user manager..." 16 | go build -o user_manager user_manager.go 17 | 18 | # Run the server 19 | run: build 20 | @echo "Starting TS Jobscanner server..." 21 | ./server 22 | 23 | # Run in production mode 24 | run-prod: build 25 | @echo "Starting TS Jobscanner server in production mode..." 26 | GIN_MODE=release ./server -config config.production.json 27 | 28 | # Clean build artifacts 29 | clean: 30 | @echo "Cleaning build artifacts..." 31 | rm -f server user_manager 32 | 33 | # Create first admin user 34 | create-admin: user-manager 35 | @echo "Creating admin user..." 36 | ./user_manager -username admin -email admin@tsunami-events.de -firstname Admin -lastname User -password "admin123" 37 | @echo "Default admin credentials: admin / admin123" 38 | @echo "Please change this password after first login!" 39 | 40 | # List users 41 | list-users: user-manager 42 | @echo "Listing all users..." 43 | ./user_manager -list 44 | 45 | # Test database connection 46 | test-db: build 47 | @echo "Testing database connection..." 48 | timeout 5s ./server || echo "Database connection test completed" 49 | 50 | # Install dependencies 51 | deps: 52 | @echo "Installing Go dependencies..." 53 | go mod download 54 | go mod tidy 55 | 56 | # Development setup 57 | dev-setup: deps build user-manager 58 | @echo "Development setup complete!" 59 | @echo "Next steps:" 60 | @echo "1. Configure your database in config.json" 61 | @echo "2. Run 'make create-admin' to create your first user" 62 | @echo "3. Run 'make run' to start the server" 63 | 64 | # Help 65 | help: 66 | @echo "TS Jobscanner Build Commands:" 67 | @echo "" 68 | @echo " build - Build the main server binary" 69 | @echo " user-manager - Build the user management tool" 70 | @echo " run - Build and run the server" 71 | @echo " run-prod - Run in production mode" 72 | @echo " clean - Remove build artifacts" 73 | @echo " create-admin - Create an admin user interactively" 74 | @echo " list-users - List all users" 75 | @echo " test-db - Test database connection" 76 | @echo " deps - Install Go dependencies" 77 | @echo " dev-setup - Complete development setup" 78 | @echo " help - Show this help message" -------------------------------------------------------------------------------- /README_DOCKER.md: -------------------------------------------------------------------------------- 1 | # Docker Deployment Guide 2 | 3 | This guide explains how to deploy the Go Barcode Webapp using Docker Compose. 4 | 5 | ## Prerequisites 6 | 7 | - Docker and Docker Compose installed on your system 8 | - Access to the external MySQL database at tsunami-events.de 9 | 10 | ## Quick Start 11 | 12 | 1. **Clone the repository** (if not already done): 13 | ```bash 14 | git clone 15 | cd go-barcode-webapp 16 | ``` 17 | 18 | 2. **Configure environment variables**: 19 | ```bash 20 | cp .env.example .env 21 | # Edit .env with your specific configuration 22 | ``` 23 | 24 | 3. **Build and start the application**: 25 | ```bash 26 | docker compose up -d 27 | ``` 28 | 29 | 4. **Access the application**: 30 | - Open your browser and go to `http://localhost:8080` 31 | - The application will be available on the port specified in your `.env` file 32 | 33 | ## Configuration 34 | 35 | ### Environment Variables 36 | 37 | The application is configured using environment variables in the `.env` file: 38 | 39 | #### Database Configuration 40 | - `DB_HOST`: Database host (default: tsunami-events.de) 41 | - `DB_PORT`: Database port (default: 3306) 42 | - `DB_NAME`: Database name (default: TS-Lager) 43 | - `DB_USERNAME`: Database username 44 | - `DB_PASSWORD`: Database password 45 | 46 | #### Server Configuration 47 | - `APP_PORT`: External port for the application (default: 8080) 48 | - `SERVER_HOST`: Internal server host (default: 0.0.0.0) 49 | - `SERVER_PORT`: Internal server port (default: 8080) 50 | - `GIN_MODE`: Application mode (release/debug) 51 | 52 | #### Security Configuration 53 | - `ENCRYPTION_KEY`: Key for encryption operations 54 | - `SESSION_TIMEOUT`: Session timeout in seconds 55 | 56 | #### Email Configuration (Optional) 57 | - `SMTP_HOST`: SMTP server host 58 | - `SMTP_PORT`: SMTP server port 59 | - `SMTP_USERNAME`: SMTP username 60 | - `SMTP_PASSWORD`: SMTP password 61 | - `FROM_EMAIL`: From email address 62 | - `FROM_NAME`: From name 63 | - `USE_TLS`: Enable TLS (true/false) 64 | 65 | ## Docker Commands 66 | 67 | ### Start the application 68 | ```bash 69 | docker compose up -d 70 | ``` 71 | 72 | ### Stop the application 73 | ```bash 74 | docker compose down 75 | ``` 76 | 77 | ### View logs 78 | ```bash 79 | docker compose logs -f go-barcode-webapp 80 | ``` 81 | 82 | ### Rebuild the application 83 | ```bash 84 | docker compose build --no-cache 85 | docker compose up -d 86 | ``` 87 | 88 | ### Update the application 89 | ```bash 90 | git pull 91 | docker compose build 92 | docker compose up -d 93 | ``` 94 | 95 | ## Persistent Data 96 | 97 | The following data is persisted using Docker volumes: 98 | - `uploads/`: File uploads 99 | - `logs/`: Application logs 100 | - `archives/`: Archive files 101 | 102 | ## Health Checks 103 | 104 | The application includes health checks: 105 | - Container health check endpoint: `http://localhost:8080/health` 106 | - Database connectivity check before application startup 107 | 108 | ## Troubleshooting 109 | 110 | ### Check container status 111 | ```bash 112 | docker compose ps 113 | ``` 114 | 115 | ### View application logs 116 | ```bash 117 | docker compose logs go-barcode-webapp 118 | ``` 119 | 120 | ### Check database connectivity 121 | ```bash 122 | docker compose logs db-health-check 123 | ``` 124 | 125 | ### Access container shell 126 | ```bash 127 | docker compose exec go-barcode-webapp sh 128 | ``` 129 | 130 | ### Verify environment variables 131 | ```bash 132 | docker compose exec go-barcode-webapp env 133 | ``` 134 | 135 | ## Production Considerations 136 | 137 | 1. **Security**: 138 | - Change the default `ENCRYPTION_KEY` in production 139 | - Use strong database passwords 140 | - Consider using Docker secrets for sensitive data 141 | 142 | 2. **Performance**: 143 | - Adjust resource limits in docker-compose.yml 144 | - Monitor container resource usage 145 | - Consider using a reverse proxy (nginx/traefik) 146 | 147 | 3. **Backups**: 148 | - The application data (uploads, logs) is stored in Docker volumes 149 | - Implement regular backups of these volumes 150 | - Database backups should be handled separately 151 | 152 | 4. **Updates**: 153 | - Test updates in a staging environment first 154 | - Use proper CI/CD pipelines for production deployments 155 | - Consider blue-green deployments for zero-downtime updates 156 | 157 | ## Network Configuration 158 | 159 | The application uses a custom Docker network (`rental-network`) to isolate containers and ensure proper communication between services. 160 | 161 | ## Monitoring 162 | 163 | Monitor the application using: 164 | - Docker container logs 165 | - Application health endpoint 166 | - Container resource usage 167 | - Database connection status 168 | 169 | ## Support 170 | 171 | For issues or questions: 172 | 1. Check the application logs first 173 | 2. Verify environment variable configuration 174 | 3. Ensure database connectivity 175 | 4. Check Docker container status -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "host": "your-database-host.example.com", 4 | "port": 3306, 5 | "database": "your_database_name", 6 | "username": "your_db_username", 7 | "password": "your_secure_database_password", 8 | "pool_size": 50 9 | }, 10 | "server": { 11 | "port": 8080, 12 | "host": "0.0.0.0" 13 | }, 14 | "ui": { 15 | "theme_dark": "professional-dark", 16 | "theme_light": "professional-light", 17 | "current_theme": "dark", 18 | "colors": { 19 | "primary": "#3b82f6", 20 | "primary_light": "#60a5fa", 21 | "primary_dark": "#1d4ed8", 22 | "secondary": "#64748b", 23 | "accent": "#3b82f6", 24 | "success": "#10b981", 25 | "danger": "#ef4444", 26 | "warning": "#f59e0b", 27 | "info": "#06b6d4", 28 | "background_dark": "#0f172a", 29 | "background_secondary": "#1e293b", 30 | "background_tertiary": "#334155", 31 | "text_primary": "#f8fafc", 32 | "text_secondary": "#cbd5e1", 33 | "text_muted": "#94a3b8", 34 | "border_primary": "#334155", 35 | "border_secondary": "#475569" 36 | }, 37 | "features": { 38 | "auto_enhance_buttons": true, 39 | "auto_enhance_badges": true, 40 | "auto_enhance_cards": true, 41 | "enhanced_animations": true, 42 | "performance_monitoring": true, 43 | "auto_navigation_state": true, 44 | "smart_theme_switching": true 45 | }, 46 | "performance": { 47 | "lazy_load_images": true, 48 | "virtual_scrolling_threshold": 50, 49 | "debounce_search": 300, 50 | "cache_api_responses": true, 51 | "preload_critical_pages": true 52 | }, 53 | "auto_save": true, 54 | "auto_save_interval": 300, 55 | "cache_timeout": 300, 56 | "window_width": 1400, 57 | "window_height": 800 58 | }, 59 | "email": { 60 | "smtp_host": "your-smtp-server.example.com", 61 | "smtp_port": 587, 62 | "smtp_username": "your_email@example.com", 63 | "smtp_password": "your_secure_email_password", 64 | "from_email": "noreply@yourcompany.com", 65 | "from_name": "Your Company RentalCore", 66 | "use_tls": true 67 | }, 68 | "invoice": { 69 | "default_tax_rate": 19.0, 70 | "default_payment_terms": 30, 71 | "auto_calculate_rental_days": true, 72 | "show_logo_on_invoice": true, 73 | "invoice_number_prefix": "INV-", 74 | "invoice_number_format": "{prefix}{year}{month}{sequence:4}", 75 | "currency_symbol": "€", 76 | "currency_code": "EUR", 77 | "date_format": "DD.MM.YYYY" 78 | }, 79 | "pdf": { 80 | "generator": "auto", 81 | "paper_size": "A4", 82 | "margins": { 83 | "top": "1cm", 84 | "bottom": "1cm", 85 | "left": "1cm", 86 | "right": "1cm" 87 | } 88 | }, 89 | "security": { 90 | "session_timeout": 3600, 91 | "password_min_length": 8, 92 | "max_login_attempts": 5, 93 | "lockout_duration": 900, 94 | "encryption_key": "CHANGE-THIS-TO-A-SECURE-256BIT-KEY-FOR-PRODUCTION" 95 | }, 96 | "logging": { 97 | "level": "info", 98 | "file": "logs/app.log", 99 | "max_size": 100, 100 | "max_backups": 5, 101 | "max_age": 30 102 | }, 103 | "backup": { 104 | "enabled": true, 105 | "interval": 86400, 106 | "retention_days": 30, 107 | "path": "backups/" 108 | } 109 | } -------------------------------------------------------------------------------- /cookies.txt: -------------------------------------------------------------------------------- 1 | # Netscape HTTP Cookie File 2 | # https://curl.se/docs/http-cookies.html 3 | # This file was generated by libcurl! Edit at your own risk. 4 | 5 | -------------------------------------------------------------------------------- /create-production-user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # JobScanner Pro - Production User Creation Script 4 | # Creates an admin user for the production deployment 5 | 6 | set -e 7 | 8 | echo "🔧 JobScanner Pro - Production User Setup" 9 | echo "" 10 | 11 | # Check if production config exists 12 | if [ ! -f "config.production.direct.json" ]; then 13 | echo "❌ Error: config.production.direct.json not found" 14 | echo "Please ensure the production config file exists in the current directory" 15 | exit 1 16 | fi 17 | 18 | # Prompt for user details 19 | echo "Creating admin user for production deployment..." 20 | echo "" 21 | 22 | read -p "👤 Username: " USERNAME 23 | read -p "📧 Email: " EMAIL 24 | read -s -p "🔒 Password: " PASSWORD 25 | echo "" 26 | read -p "👤 First Name (optional): " FIRSTNAME 27 | read -p "👤 Last Name (optional): " LASTNAME 28 | 29 | echo "" 30 | echo "Creating user with production database..." 31 | 32 | # Create user using production config 33 | go run create_user.go \ 34 | -config=config.production.direct.json \ 35 | -username="$USERNAME" \ 36 | -email="$EMAIL" \ 37 | -password="$PASSWORD" \ 38 | -firstname="$FIRSTNAME" \ 39 | -lastname="$LASTNAME" 40 | 41 | echo "" 42 | echo "✅ Production admin user created successfully!" 43 | echo "" 44 | echo "🌐 You can now log in to the production application at:" 45 | echo " http://your-server:8080/login" 46 | echo "" 47 | echo "📝 Credentials:" 48 | echo " Username: $USERNAME" 49 | echo " Email: $EMAIL" 50 | echo "" 51 | echo "🔒 For security, please:" 52 | echo " 1. Use a strong password" 53 | echo " 2. Enable HTTPS in production" 54 | echo " 3. Restrict access to the application" -------------------------------------------------------------------------------- /database/test_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ============================================================================ 4 | # RentalCore Database Setup Test Script 5 | # ============================================================================ 6 | # This script tests the database setup procedure without affecting production 7 | 8 | set -e # Exit on any error 9 | 10 | echo "🧪 RentalCore Database Setup Test" 11 | echo "==================================" 12 | 13 | # Configuration 14 | TEST_DB="rentalcore_test_$(date +%s)" 15 | TEST_USER="test_user_$(date +%s)" 16 | TEST_PASS="test_password_123" 17 | MYSQL_ROOT_PASS="${MYSQL_ROOT_PASSWORD:-}" 18 | 19 | echo "📋 Test Configuration:" 20 | echo " Database: $TEST_DB" 21 | echo " User: $TEST_USER" 22 | echo " Password: [hidden]" 23 | echo "" 24 | 25 | # Function to cleanup test database 26 | cleanup() { 27 | echo "🧹 Cleaning up test database..." 28 | mysql -u root ${MYSQL_ROOT_PASS:+-p$MYSQL_ROOT_PASS} -e "DROP DATABASE IF EXISTS $TEST_DB;" 2>/dev/null || true 29 | mysql -u root ${MYSQL_ROOT_PASS:+-p$MYSQL_ROOT_PASS} -e "DROP USER IF EXISTS '$TEST_USER'@'%';" 2>/dev/null || true 30 | echo " Cleanup complete" 31 | } 32 | 33 | # Set trap to cleanup on exit 34 | trap cleanup EXIT 35 | 36 | echo "🚀 Step 1: Creating test database and user..." 37 | mysql -u root ${MYSQL_ROOT_PASS:+-p$MYSQL_ROOT_PASS} << EOF 38 | CREATE DATABASE $TEST_DB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 39 | CREATE USER '$TEST_USER'@'%' IDENTIFIED BY '$TEST_PASS'; 40 | GRANT ALL PRIVILEGES ON $TEST_DB.* TO '$TEST_USER'@'%'; 41 | FLUSH PRIVILEGES; 42 | EOF 43 | echo " ✓ Database and user created successfully" 44 | 45 | echo "" 46 | echo "🏗️ Step 2: Importing database schema..." 47 | if mysql -u $TEST_USER -p$TEST_PASS $TEST_DB < database/rentalcore_setup.sql; then 48 | echo " ✓ Schema imported successfully" 49 | else 50 | echo " ✗ Schema import failed" 51 | exit 1 52 | fi 53 | 54 | echo "" 55 | echo "🔍 Step 3: Running validation tests..." 56 | if mysql -u $TEST_USER -p$TEST_PASS $TEST_DB < database/validate_setup.sql; then 57 | echo " ✓ All validation tests passed" 58 | else 59 | echo " ⚠️ Some validation tests failed (check output above)" 60 | fi 61 | 62 | echo "" 63 | echo "📊 Step 4: Testing sample queries..." 64 | 65 | # Test connection and basic query 66 | echo " Testing database connection..." 67 | CUSTOMER_COUNT=$(mysql -u $TEST_USER -p$TEST_PASS $TEST_DB -N -e "SELECT COUNT(*) FROM customers;") 68 | echo " Found $CUSTOMER_COUNT sample customers" 69 | 70 | DEVICE_COUNT=$(mysql -u $TEST_USER -p$TEST_PASS $TEST_DB -N -e "SELECT COUNT(*) FROM devices;") 71 | echo " Found $DEVICE_COUNT sample devices" 72 | 73 | JOB_COUNT=$(mysql -u $TEST_USER -p$TEST_PASS $TEST_DB -N -e "SELECT COUNT(*) FROM jobs;") 74 | echo " Found $JOB_COUNT sample jobs" 75 | 76 | # Test admin user exists 77 | ADMIN_EXISTS=$(mysql -u $TEST_USER -p$TEST_PASS $TEST_DB -N -e "SELECT COUNT(*) FROM users WHERE username='admin';") 78 | if [ "$ADMIN_EXISTS" = "1" ]; then 79 | echo " ✓ Default admin user exists" 80 | else 81 | echo " ✗ Default admin user missing" 82 | exit 1 83 | fi 84 | 85 | echo "" 86 | echo "🎯 Step 5: Testing application-specific queries..." 87 | 88 | # Test equipment availability query (used by device management) 89 | AVAILABLE_DEVICES=$(mysql -u $TEST_USER -p$TEST_PASS $TEST_DB -N -e "SELECT COUNT(*) FROM devices WHERE status='available';") 90 | echo " Available devices: $AVAILABLE_DEVICES" 91 | 92 | # Test revenue calculation query (used by analytics) 93 | TOTAL_REVENUE=$(mysql -u $TEST_USER -p$TEST_PASS $TEST_DB -N -e "SELECT COALESCE(SUM(COALESCE(final_revenue, revenue)), 0) FROM jobs WHERE endDate IS NOT NULL;") 94 | echo " Total sample revenue: EUR $TOTAL_REVENUE" 95 | 96 | # Test customer search query (used by customer management) 97 | CUSTOMER_WITH_JOBS=$(mysql -u $TEST_USER -p$TEST_PASS $TEST_DB -N -e "SELECT COUNT(DISTINCT customerID) FROM jobs;") 98 | echo " Customers with jobs: $CUSTOMER_WITH_JOBS" 99 | 100 | echo "" 101 | echo "✅ Database Setup Test Results:" 102 | echo "==============================" 103 | echo "✓ Database creation: SUCCESS" 104 | echo "✓ Schema import: SUCCESS" 105 | echo "✓ Sample data: SUCCESS" 106 | echo "✓ Foreign keys: SUCCESS" 107 | echo "✓ Indexes: SUCCESS" 108 | echo "✓ Application queries: SUCCESS" 109 | echo "" 110 | echo "🎉 RentalCore database setup is working correctly!" 111 | echo "" 112 | echo "📝 Next Steps for Production:" 113 | echo "1. Use a secure database password" 114 | echo "2. Change the default admin password (admin/admin123)" 115 | echo "3. Remove or replace sample data with real data" 116 | echo "4. Configure regular backups" 117 | echo "5. Set up monitoring and alerting" 118 | echo "" 119 | echo "🔗 Helpful Commands:" 120 | echo " docker-compose up -d # Start RentalCore" 121 | echo " docker-compose logs -f rentalcore # View application logs" 122 | echo " mysql -u your_user -p your_database # Connect to database" 123 | echo "" 124 | echo "📖 Documentation: https://github.com/nbt4/RentalCore" 125 | 126 | # Note: cleanup() will run automatically on exit due to trap -------------------------------------------------------------------------------- /database/validate_setup.sql: -------------------------------------------------------------------------------- 1 | -- ============================================================================ 2 | -- RentalCore Database Setup Validation Script 3 | -- ============================================================================ 4 | -- This script validates that the database setup was completed correctly 5 | 6 | -- Test 1: Verify all required tables exist 7 | SELECT 'Testing table existence...' as test_phase; 8 | 9 | SELECT 10 | COUNT(*) as total_tables, 11 | CASE 12 | WHEN COUNT(*) >= 12 THEN 'PASS' 13 | ELSE 'FAIL' 14 | END as table_test_result 15 | FROM information_schema.tables 16 | WHERE table_schema = DATABASE(); 17 | 18 | -- Test 2: Check table structures for key tables 19 | SELECT 'Testing table structures...' as test_phase; 20 | 21 | -- Verify customers table structure 22 | SELECT 'customers' as table_name, COUNT(*) as columns 23 | FROM information_schema.columns 24 | WHERE table_schema = DATABASE() AND table_name = 'customers'; 25 | 26 | -- Verify jobs table structure 27 | SELECT 'jobs' as table_name, COUNT(*) as columns 28 | FROM information_schema.columns 29 | WHERE table_schema = DATABASE() AND table_name = 'jobs'; 30 | 31 | -- Verify devices table structure 32 | SELECT 'devices' as table_name, COUNT(*) as columns 33 | FROM information_schema.columns 34 | WHERE table_schema = DATABASE() AND table_name = 'devices'; 35 | 36 | -- Test 3: Verify foreign key relationships 37 | SELECT 'Testing foreign key constraints...' as test_phase; 38 | 39 | SELECT 40 | COUNT(*) as total_foreign_keys, 41 | CASE 42 | WHEN COUNT(*) >= 5 THEN 'PASS' 43 | ELSE 'FAIL' 44 | END as fk_test_result 45 | FROM information_schema.key_column_usage 46 | WHERE table_schema = DATABASE() 47 | AND referenced_table_name IS NOT NULL; 48 | 49 | -- Test 4: Check sample data was imported 50 | SELECT 'Testing sample data import...' as test_phase; 51 | 52 | -- Count sample records 53 | SELECT 'Sample Data Check' as check_name, 54 | (SELECT COUNT(*) FROM customers) as customers, 55 | (SELECT COUNT(*) FROM categories) as categories, 56 | (SELECT COUNT(*) FROM products) as products, 57 | (SELECT COUNT(*) FROM devices) as devices, 58 | (SELECT COUNT(*) FROM statuses) as statuses, 59 | (SELECT COUNT(*) FROM jobs) as jobs, 60 | (SELECT COUNT(*) FROM users) as users; 61 | 62 | -- Test 5: Verify critical indexes exist 63 | SELECT 'Testing index creation...' as test_phase; 64 | 65 | SELECT 66 | table_name, 67 | index_name, 68 | non_unique, 69 | column_name 70 | FROM information_schema.statistics 71 | WHERE table_schema = DATABASE() 72 | AND table_name IN ('customers', 'jobs', 'devices', 'jobdevices') 73 | ORDER BY table_name, index_name; 74 | 75 | -- Test 6: Test basic queries that the application will use 76 | SELECT 'Testing application queries...' as test_phase; 77 | 78 | -- Test equipment query 79 | SELECT 'Equipment Query Test' as test_type, 80 | d.deviceID, 81 | p.name as product_name, 82 | c.name as category_name, 83 | d.status 84 | FROM devices d 85 | JOIN products p ON d.productID = p.productID 86 | JOIN categories c ON p.categoryID = c.categoryID 87 | LIMIT 3; 88 | 89 | -- Test customer query 90 | SELECT 'Customer Query Test' as test_type, 91 | customerID, 92 | COALESCE(companyname, CONCAT(firstname, ' ', lastname)) as customer_name, 93 | email 94 | FROM customers 95 | LIMIT 3; 96 | 97 | -- Test job with revenue query 98 | SELECT 'Job Revenue Query Test' as test_type, 99 | j.jobID, 100 | c.companyname as customer, 101 | j.startDate, 102 | j.endDate, 103 | COALESCE(j.final_revenue, j.revenue) as revenue 104 | FROM jobs j 105 | JOIN customers c ON j.customerID = c.customerID 106 | WHERE COALESCE(j.final_revenue, j.revenue) > 0 107 | LIMIT 3; 108 | 109 | -- Test device availability query 110 | SELECT 'Device Availability Test' as test_type, 111 | status, 112 | COUNT(*) as device_count 113 | FROM devices 114 | GROUP BY status; 115 | 116 | -- Final validation summary 117 | SELECT 'VALIDATION COMPLETE' as status, 118 | DATABASE() as database_name, 119 | NOW() as validation_time, 120 | CASE 121 | WHEN (SELECT COUNT(*) FROM customers) > 0 122 | AND (SELECT COUNT(*) FROM devices) > 0 123 | AND (SELECT COUNT(*) FROM jobs) > 0 124 | AND (SELECT COUNT(*) FROM users) > 0 125 | THEN 'DATABASE SETUP SUCCESSFUL ✓' 126 | ELSE 'DATABASE SETUP INCOMPLETE ✗' 127 | END as final_result; -------------------------------------------------------------------------------- /deploy-production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # JobScanner Pro - Production Deployment Script 4 | 5 | set -e 6 | 7 | echo "🚀 Deploying JobScanner Pro to Production..." 8 | 9 | # Build the application for production 10 | echo "📦 Building application..." 11 | go build -o server ./cmd/server 12 | 13 | # Create logs directory 14 | mkdir -p logs 15 | 16 | # Check if production config exists 17 | if [ ! -f "config.production.direct.json" ]; then 18 | echo "⚠️ Production config file not found!" 19 | echo "Please ensure config.production.direct.json exists with your production settings" 20 | echo "You can copy and modify config.production.json as a template" 21 | fi 22 | 23 | # Install systemd service (requires root) 24 | if [ "$EUID" -eq 0 ]; then 25 | echo "🔧 Installing systemd service..." 26 | cp jobscanner.service /etc/systemd/system/ 27 | systemctl daemon-reload 28 | systemctl enable jobscanner 29 | echo "✅ Systemd service installed and enabled" 30 | 31 | echo "📝 To start the service, run:" 32 | echo " sudo systemctl start jobscanner" 33 | echo " sudo systemctl status jobscanner" 34 | else 35 | echo "⚠️ Run as root to install systemd service:" 36 | echo " sudo ./deploy-production.sh" 37 | fi 38 | 39 | echo "" 40 | echo "✅ Configuration:" 41 | echo " 📄 Using config file: config.production.direct.json" 42 | echo " 🌐 Server will run on: http://0.0.0.0:8080" 43 | echo " 📝 Logs location: logs/production.log" 44 | echo "" 45 | echo "👤 Create admin user for production:" 46 | echo " ./create-production-user.sh" 47 | echo "" 48 | echo "🚀 To start manually:" 49 | echo " ./start-production.sh" 50 | echo "" 51 | echo "📋 User Management Access:" 52 | echo " 1. Start the application" 53 | echo " 2. Log in with admin credentials" 54 | echo " 3. Navigate to: http://your-server:8080/users" 55 | echo " 4. Click 'Create New User' to add users" 56 | echo "" 57 | echo "✅ Deployment complete!" -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # RentalCore Docker Compose Configuration Example 3 | # ============================================================================ 4 | # Copy this file to docker-compose.yml and customize for your environment 5 | # Make sure to create a .env file with your actual credentials before running 6 | 7 | version: '3.8' 8 | 9 | services: 10 | rentalcore: 11 | # Use the pre-built image from Docker Hub 12 | image: nbt4/rentalcore:latest 13 | 14 | # Or build from source (uncomment the lines below and comment the image line) 15 | # build: 16 | # context: . 17 | # dockerfile: Dockerfile 18 | 19 | container_name: rentalcore-app 20 | hostname: rentalcore 21 | restart: unless-stopped 22 | 23 | ports: 24 | # Map container port 8080 to host port (configurable via APP_PORT) 25 | - "${APP_PORT:-8080}:8080" 26 | 27 | environment: 28 | # ================================ 29 | # Database Configuration 30 | # ================================ 31 | - DB_HOST=${DB_HOST} 32 | - DB_PORT=${DB_PORT:-3306} 33 | - DB_NAME=${DB_NAME} 34 | - DB_USERNAME=${DB_USERNAME} 35 | - DB_PASSWORD=${DB_PASSWORD} 36 | 37 | # ================================ 38 | # Server Configuration 39 | # ================================ 40 | - SERVER_HOST=${SERVER_HOST:-0.0.0.0} 41 | - SERVER_PORT=${SERVER_PORT:-8080} 42 | 43 | # ================================ 44 | # Application Mode 45 | # ================================ 46 | # Set to 'release' for production, 'debug' for development 47 | - GIN_MODE=${GIN_MODE:-release} 48 | 49 | # ================================ 50 | # Security Configuration 51 | # ================================ 52 | - ENCRYPTION_KEY=${ENCRYPTION_KEY} 53 | - SESSION_TIMEOUT=${SESSION_TIMEOUT:-3600} 54 | 55 | # ================================ 56 | # Email Configuration (Optional) 57 | # ================================ 58 | - SMTP_HOST=${SMTP_HOST:-} 59 | - SMTP_PORT=${SMTP_PORT:-587} 60 | - SMTP_USERNAME=${SMTP_USERNAME:-} 61 | - SMTP_PASSWORD=${SMTP_PASSWORD:-} 62 | - FROM_EMAIL=${FROM_EMAIL:-noreply@rentalcore.com} 63 | - FROM_NAME=${FROM_NAME:-RentalCore} 64 | - USE_TLS=${USE_TLS:-true} 65 | 66 | # ================================ 67 | # Invoice Configuration 68 | # ================================ 69 | - DEFAULT_TAX_RATE=${DEFAULT_TAX_RATE:-19.0} 70 | - DEFAULT_PAYMENT_TERMS=${DEFAULT_PAYMENT_TERMS:-30} 71 | - CURRENCY_SYMBOL=${CURRENCY_SYMBOL:-€} 72 | - CURRENCY_CODE=${CURRENCY_CODE:-EUR} 73 | 74 | # ================================ 75 | # Logging Configuration 76 | # ================================ 77 | - LOG_LEVEL=${LOG_LEVEL:-info} 78 | - LOG_FILE=${LOG_FILE:-logs/app.log} 79 | 80 | volumes: 81 | # Persistent data volumes for uploads, logs, and archives 82 | - rentalcore_uploads:/app/uploads 83 | - rentalcore_logs:/app/logs 84 | - rentalcore_archives:/app/archives 85 | 86 | # Optional: Mount config file if using file-based configuration 87 | # - ./config.json:/app/config.json:ro 88 | 89 | # Health check to ensure the application is running 90 | healthcheck: 91 | test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] 92 | interval: 30s 93 | timeout: 10s 94 | retries: 3 95 | start_period: 60s 96 | 97 | # Optional: Add to networks for reverse proxy or service discovery 98 | # networks: 99 | # - rentalcore_network 100 | # - traefik_proxy # If using Traefik 101 | 102 | # Optional: Add labels for reverse proxy configuration 103 | # labels: 104 | # - "traefik.enable=true" 105 | # - "traefik.http.routers.rentalcore.rule=Host(`rentalcore.yourdomain.com`)" 106 | # - "traefik.http.services.rentalcore.loadbalancer.server.port=8080" 107 | # - "traefik.http.routers.rentalcore.tls.certresolver=letsencrypt" 108 | 109 | # ============================================================================ 110 | # Volumes for persistent data 111 | # ============================================================================ 112 | volumes: 113 | rentalcore_uploads: 114 | driver: local 115 | # Optional: specify host path for backup purposes 116 | # driver_opts: 117 | # type: none 118 | # o: bind 119 | # device: /opt/rentalcore/uploads 120 | 121 | rentalcore_logs: 122 | driver: local 123 | # Optional: specify host path for log monitoring 124 | # driver_opts: 125 | # type: none 126 | # o: bind 127 | # device: /opt/rentalcore/logs 128 | 129 | rentalcore_archives: 130 | driver: local 131 | 132 | # ============================================================================ 133 | # Networks (optional) 134 | # ============================================================================ 135 | # networks: 136 | # rentalcore_network: 137 | # driver: bridge 138 | # traefik_proxy: 139 | # external: true 140 | 141 | # ============================================================================ 142 | # Usage Instructions: 143 | # ============================================================================ 144 | # 1. Copy this file to docker-compose.yml 145 | # 2. Copy .env.example to .env and configure your values 146 | # 3. Run: docker-compose up -d 147 | # 4. Access the application at http://localhost:8080 (or your configured port) 148 | # 5. For updates: docker-compose pull && docker-compose up -d 149 | # ============================================================================ -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | # Production Docker Compose Configuration for RentalCore 2 | # This version uses the pre-built Docker image from Docker Hub 3 | # No building required - just configure your .env file and run! 4 | 5 | services: 6 | rentalcore: 7 | # Use the image from Docker Hub instead of building 8 | image: nbt4/rentalcore:latest 9 | container_name: rentalcore 10 | hostname: rentalcore 11 | restart: unless-stopped 12 | 13 | ports: 14 | - "${APP_PORT:-8080}:8080" 15 | 16 | environment: 17 | # Database Configuration 18 | - DB_HOST=${DB_HOST} 19 | - DB_PORT=${DB_PORT:-3306} 20 | - DB_NAME=${DB_NAME} 21 | - DB_USERNAME=${DB_USERNAME} 22 | - DB_PASSWORD=${DB_PASSWORD} 23 | 24 | # Server Configuration 25 | - SERVER_HOST=${SERVER_HOST:-0.0.0.0} 26 | - SERVER_PORT=${SERVER_PORT:-8080} 27 | 28 | # Application Mode 29 | - GIN_MODE=${GIN_MODE:-release} 30 | 31 | # Security Configuration 32 | - ENCRYPTION_KEY=${ENCRYPTION_KEY} 33 | - SESSION_TIMEOUT=${SESSION_TIMEOUT:-3600} 34 | 35 | # Email Configuration (optional) 36 | - SMTP_HOST=${SMTP_HOST:-} 37 | - SMTP_PORT=${SMTP_PORT:-587} 38 | - SMTP_USERNAME=${SMTP_USERNAME:-} 39 | - SMTP_PASSWORD=${SMTP_PASSWORD:-} 40 | - FROM_EMAIL=${FROM_EMAIL:-noreply@rentalcore.com} 41 | - FROM_NAME=${FROM_NAME:-RentalCore} 42 | - USE_TLS=${USE_TLS:-true} 43 | 44 | # Invoice Configuration 45 | - DEFAULT_TAX_RATE=${DEFAULT_TAX_RATE:-19.0} 46 | - DEFAULT_PAYMENT_TERMS=${DEFAULT_PAYMENT_TERMS:-30} 47 | - CURRENCY_SYMBOL=${CURRENCY_SYMBOL:-€} 48 | - CURRENCY_CODE=${CURRENCY_CODE:-EUR} 49 | 50 | # Logging Configuration 51 | - LOG_LEVEL=${LOG_LEVEL:-info} 52 | - LOG_FILE=${LOG_FILE:-logs/app.log} 53 | 54 | volumes: 55 | # Persistent data volumes 56 | - rentalcore_uploads:/app/uploads 57 | - rentalcore_logs:/app/logs 58 | - rentalcore_archives:/app/archives 59 | 60 | depends_on: 61 | - db-health-check 62 | 63 | networks: 64 | - rentalcore-network 65 | 66 | healthcheck: 67 | test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] 68 | interval: 30s 69 | timeout: 10s 70 | retries: 3 71 | start_period: 60s 72 | 73 | # Database health check service (for external databases) 74 | db-health-check: 75 | image: mysql:8.0 76 | container_name: rentalcore-db-check 77 | command: > 78 | sh -c " 79 | echo 'Checking database connectivity...' && 80 | mysql -h$$DB_HOST -P$$DB_PORT -u$$DB_USERNAME -p$$DB_PASSWORD -e 'SELECT 1;' $$DB_NAME && 81 | echo 'Database is accessible!' 82 | " 83 | environment: 84 | - DB_HOST=${DB_HOST} 85 | - DB_PORT=${DB_PORT:-3306} 86 | - DB_NAME=${DB_NAME} 87 | - DB_USERNAME=${DB_USERNAME} 88 | - DB_PASSWORD=${DB_PASSWORD} 89 | networks: 90 | - rentalcore-network 91 | restart: "no" 92 | 93 | volumes: 94 | rentalcore_uploads: 95 | driver: local 96 | rentalcore_logs: 97 | driver: local 98 | rentalcore_archives: 99 | driver: local 100 | 101 | networks: 102 | rentalcore-network: 103 | driver: bridge -------------------------------------------------------------------------------- /docs/ADMIN_GUIDE.md: -------------------------------------------------------------------------------- 1 | # RentalCore Administrator Guide 2 | 3 | ## System Administration 4 | 5 | ### Initial Setup 6 | 1. **Change Default Password**: First priority after installation 7 | 2. **Configure Database**: Ensure proper database credentials and connectivity 8 | 3. **Set Up SSL/TLS**: Configure HTTPS for production environments 9 | 4. **Configure Email**: Set up SMTP for notifications (optional) 10 | 11 | ### User Management 12 | 13 | #### Creating Admin Users 14 | 1. Navigate to "Users" in the admin panel 15 | 2. Click "Add New User" 16 | 3. Set role to "Admin" for full system access 17 | 4. Require strong password and enable 2FA 18 | 19 | #### User Roles 20 | - **Admin**: Full system access, user management, system configuration 21 | - **Manager**: Job and equipment management, customer access 22 | - **User**: Read-only access to assigned jobs and equipment 23 | 24 | #### Security Policies 25 | - Enforce strong password requirements 26 | - Enable session timeout (default: 1 hour) 27 | - Implement account lockout after failed attempts 28 | - Regular password rotation policy 29 | 30 | ### System Configuration 31 | 32 | #### Environment Variables 33 | Critical settings in `.env` file: 34 | ```bash 35 | # Security 36 | ENCRYPTION_KEY=your-256-bit-key 37 | SESSION_SECRET=your-session-secret 38 | SESSION_TIMEOUT=3600 39 | 40 | # Database 41 | DB_HOST=database-host 42 | DB_NAME=rentalcore 43 | DB_USERNAME=rentalcore_user 44 | DB_PASSWORD=secure-password 45 | 46 | # Features 47 | ENABLE_2FA=true 48 | ENABLE_AUDIT_LOG=true 49 | ``` 50 | 51 | #### Application Configuration 52 | Key settings in `config.json`: 53 | - Company branding and theme 54 | - Currency and localization 55 | - Feature toggles 56 | - Performance settings 57 | 58 | ### Database Administration 59 | 60 | #### Backup Strategy 61 | ```bash 62 | # Daily automated backup 63 | mysqldump -u username -p rentalcore > backup_$(date +%Y%m%d).sql 64 | 65 | # Docker backup 66 | docker exec mysql-container mysqldump -u root -p rentalcore > backup.sql 67 | ``` 68 | 69 | #### Database Maintenance 70 | - Regular optimization of analytics cache 71 | - Cleanup of old log entries 72 | - Index maintenance for performance 73 | - Monitor database size and growth 74 | 75 | ### Monitoring and Logging 76 | 77 | #### Application Logs 78 | Monitor these log files: 79 | - `/app/logs/application.log` - General application events 80 | - `/app/logs/security.log` - Security events and failed logins 81 | - `/app/logs/error.log` - Application errors and exceptions 82 | 83 | #### Health Monitoring 84 | - Use `/health` endpoint for application health checks 85 | - Monitor response times and error rates 86 | - Set up alerts for critical issues 87 | - Track resource usage (CPU, memory, disk) 88 | 89 | ### Security Administration 90 | 91 | #### SSL/TLS Configuration 92 | ```yaml 93 | # Docker Compose with Traefik 94 | labels: 95 | - "traefik.enable=true" 96 | - "traefik.http.routers.rentalcore.rule=Host(`rental.yourdomain.com`)" 97 | - "traefik.http.routers.rentalcore.tls.certresolver=letsencrypt" 98 | ``` 99 | 100 | #### Security Headers 101 | Implement these headers in your reverse proxy: 102 | ``` 103 | Strict-Transport-Security: max-age=31536000 104 | X-Content-Type-Options: nosniff 105 | X-Frame-Options: DENY 106 | X-XSS-Protection: 1; mode=block 107 | Content-Security-Policy: default-src 'self' 108 | ``` 109 | 110 | #### Audit Logging 111 | All administrative actions are logged including: 112 | - User creation and deletion 113 | - Password changes 114 | - System configuration changes 115 | - Data exports and sensitive operations 116 | 117 | ### Performance Optimization 118 | 119 | #### Database Performance 120 | - Monitor slow query log 121 | - Optimize indexes based on query patterns 122 | - Configure appropriate connection pool size 123 | - Regular ANALYZE TABLE maintenance 124 | 125 | #### Application Performance 126 | - Monitor memory usage and optimize if needed 127 | - Configure appropriate cache settings 128 | - Optimize image and file uploads 129 | - Use CDN for static assets in production 130 | 131 | ### Backup and Recovery 132 | 133 | #### Backup Checklist 134 | - [ ] Daily database backups 135 | - [ ] Weekly full system backups 136 | - [ ] Monthly backup verification 137 | - [ ] Offsite backup storage 138 | - [ ] Document recovery procedures 139 | 140 | #### Recovery Procedures 141 | 1. **Database Recovery**: 142 | ```bash 143 | mysql -u username -p rentalcore < backup.sql 144 | ``` 145 | 146 | 2. **File Recovery**: 147 | ```bash 148 | tar -xzf uploads_backup.tar.gz -C /app/uploads/ 149 | ``` 150 | 151 | 3. **Full System Recovery**: 152 | - Restore database from backup 153 | - Restore uploaded files 154 | - Verify application configuration 155 | - Test all critical functions 156 | 157 | ### Maintenance Tasks 158 | 159 | #### Daily Tasks 160 | - Monitor application logs 161 | - Check system health status 162 | - Verify backup completion 163 | - Review security events 164 | 165 | #### Weekly Tasks 166 | - Analyze performance metrics 167 | - Review user activity reports 168 | - Update system documentation 169 | - Plan capacity requirements 170 | 171 | #### Monthly Tasks 172 | - Security vulnerability assessment 173 | - System performance review 174 | - Backup recovery testing 175 | - User access review 176 | 177 | ### Troubleshooting 178 | 179 | #### Common Admin Issues 180 | 181 | **Application Won't Start** 182 | - Check environment variables 183 | - Verify database connectivity 184 | - Review application logs 185 | - Validate configuration files 186 | 187 | **Performance Issues** 188 | - Monitor database query performance 189 | - Check system resources (CPU, memory, disk) 190 | - Review analytics cache settings 191 | - Optimize database indexes 192 | 193 | **Security Concerns** 194 | - Review audit logs for suspicious activity 195 | - Check failed login attempts 196 | - Verify SSL/TLS configuration 197 | - Validate user permissions 198 | 199 | #### Log Analysis 200 | ```bash 201 | # Check recent errors 202 | tail -f /app/logs/error.log 203 | 204 | # Monitor security events 205 | grep "SECURITY" /app/logs/application.log 206 | 207 | # Analyze performance 208 | grep "SLOW_QUERY" /app/logs/application.log 209 | ``` 210 | 211 | ### System Updates 212 | 213 | #### Update Procedure 214 | 1. **Backup Current System** 215 | - Database backup 216 | - Configuration backup 217 | - File backup 218 | 219 | 2. **Update Application** 220 | ```bash 221 | docker pull nbt4/rentalcore:latest 222 | docker-compose up -d 223 | ``` 224 | 225 | 3. **Verify Update** 226 | - Check application health 227 | - Test critical functions 228 | - Verify data integrity 229 | 230 | 4. **Rollback if Needed** 231 | ```bash 232 | docker-compose down 233 | docker-compose up -d nbt4/rentalcore:previous-version 234 | ``` 235 | 236 | ### Best Practices 237 | 238 | #### Security Best Practices 239 | - Regular security updates 240 | - Strong authentication policies 241 | - Principle of least privilege 242 | - Regular security audits 243 | - Incident response procedures 244 | 245 | #### Operational Best Practices 246 | - Automated monitoring and alerts 247 | - Regular backup testing 248 | - Capacity planning 249 | - Documentation maintenance 250 | - Change management procedures -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # RentalCore API Documentation 2 | 3 | ## Overview 4 | RentalCore provides a comprehensive REST API for managing equipment rentals, customers, and analytics. 5 | 6 | ## Base URL 7 | ``` 8 | http://localhost:8080/api/v1 9 | ``` 10 | 11 | ## Authentication 12 | All API endpoints require authentication via session cookies or API tokens. 13 | 14 | ## Core Endpoints 15 | 16 | ### Jobs Management 17 | - `GET /api/v1/jobs` - List all jobs 18 | - `POST /api/v1/jobs` - Create new job 19 | - `PUT /api/v1/jobs/:id` - Update job 20 | - `DELETE /api/v1/jobs/:id` - Delete job 21 | 22 | ### Device Management 23 | - `GET /api/v1/devices` - List all devices 24 | - `POST /api/v1/devices` - Create new device 25 | - `PUT /api/v1/devices/:id` - Update device 26 | - `DELETE /api/v1/devices/:id` - Delete device 27 | 28 | ### Customer Management 29 | - `GET /api/v1/customers` - List all customers 30 | - `POST /api/v1/customers` - Create new customer 31 | - `PUT /api/v1/customers/:id` - Update customer 32 | - `DELETE /api/v1/customers/:id` - Delete customer 33 | 34 | ### Analytics Endpoints 35 | - `GET /analytics` - Main analytics dashboard 36 | - `GET /analytics/devices/:deviceId` - Individual device analytics 37 | - `GET /analytics/export` - Export analytics data 38 | 39 | ## Response Format 40 | All API responses follow this structure: 41 | ```json 42 | { 43 | "status": "success|error", 44 | "data": {}, 45 | "message": "Optional message" 46 | } 47 | ``` 48 | 49 | ## Error Codes 50 | - `200` - Success 51 | - `400` - Bad Request 52 | - `401` - Unauthorized 53 | - `404` - Not Found 54 | - `500` - Internal Server Error -------------------------------------------------------------------------------- /docs/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # RentalCore System Architecture 2 | 3 | ## Overview 4 | RentalCore follows a clean architecture pattern with separation of concerns and modular design. 5 | 6 | ## Architecture Layers 7 | 8 | ### 1. Presentation Layer 9 | - **Web Templates**: HTML templates with Go templating 10 | - **Static Assets**: CSS, JavaScript, images 11 | - **API Endpoints**: RESTful API for external integration 12 | 13 | ### 2. Handler Layer 14 | - **HTTP Handlers**: Request processing and response generation 15 | - **Middleware**: Authentication, logging, CORS, rate limiting 16 | - **Input Validation**: Request validation and sanitization 17 | 18 | ### 3. Service Layer 19 | - **Business Logic**: Core rental management logic 20 | - **Data Processing**: Analytics calculations and reporting 21 | - **File Management**: Document upload and processing 22 | 23 | ### 4. Data Layer 24 | - **GORM ORM**: Database abstraction and migrations 25 | - **MySQL Database**: Primary data storage 26 | - **File Storage**: Local filesystem for uploads 27 | 28 | ## Directory Structure 29 | 30 | ``` 31 | rentalcore/ 32 | ├── cmd/server/ # Application entry point 33 | ├── internal/ 34 | │ ├── handlers/ # HTTP request handlers 35 | │ │ ├── analytics_handler.go 36 | │ │ ├── device_handler.go 37 | │ │ └── customer_handler.go 38 | │ ├── models/ # Database models 39 | │ │ ├── job.go 40 | │ │ ├── device.go 41 | │ │ └── customer.go 42 | │ ├── services/ # Business logic 43 | │ └── middleware/ # HTTP middleware 44 | ├── web/ 45 | │ ├── templates/ # HTML templates 46 | │ └── static/ # Static assets 47 | ├── migrations/ # Database migrations 48 | └── docs/ # Documentation 49 | ``` 50 | 51 | ## Database Schema 52 | 53 | ### Core Tables 54 | - **customers**: Customer information and contacts 55 | - **jobs**: Rental jobs and bookings 56 | - **devices**: Equipment inventory 57 | - **jobdevices**: Many-to-many relationship for job assignments 58 | - **products**: Equipment types and categories 59 | - **users**: System users and authentication 60 | 61 | ### Supporting Tables 62 | - **categories**: Equipment categories 63 | - **statuses**: Job status definitions 64 | - **analytics_cache**: Performance optimization for reports 65 | 66 | ## Design Patterns 67 | 68 | ### Repository Pattern 69 | - Abstraction layer for data access 70 | - Testable data operations 71 | - Database-agnostic queries 72 | 73 | ### MVC Pattern 74 | - Model: Database entities and business logic 75 | - View: HTML templates and user interface 76 | - Controller: HTTP handlers and request processing 77 | 78 | ### Dependency Injection 79 | - Service dependencies injected at startup 80 | - Testable and maintainable code 81 | - Configuration-driven initialization 82 | 83 | ## Performance Considerations 84 | 85 | ### Database Optimization 86 | - Proper indexing on frequently queried columns 87 | - Connection pooling (50 connections default) 88 | - Query optimization for analytics 89 | 90 | ### Caching Strategy 91 | - Analytics data caching for improved performance 92 | - Static asset caching 93 | - Template compilation caching 94 | 95 | ### Scalability 96 | - Horizontal scaling ready 97 | - Stateless application design 98 | - External session storage support 99 | 100 | ## Security Architecture 101 | 102 | ### Authentication Flow 103 | 1. User login with credentials 104 | 2. Session creation and cookie setting 105 | 3. Request authentication via middleware 106 | 4. Role-based authorization 107 | 108 | ### Data Flow Security 109 | - Input validation at handler level 110 | - SQL injection prevention via ORM 111 | - Output encoding for XSS prevention 112 | - CSRF protection for state-changing operations -------------------------------------------------------------------------------- /docs/CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # RentalCore Configuration Guide 2 | 3 | ## Environment Variables (.env) 4 | 5 | ### Database Configuration 6 | ```bash 7 | # Database Connection 8 | DB_HOST=your-database-host.com 9 | DB_PORT=3306 10 | DB_NAME=rentalcore 11 | DB_USERNAME=rentalcore_user 12 | DB_PASSWORD=secure_password_here 13 | DB_CHARSET=utf8mb4 14 | DB_PARSE_TIME=true 15 | DB_LOC=UTC 16 | 17 | # Connection Pool Settings 18 | DB_MAX_OPEN_CONNS=50 19 | DB_MAX_IDLE_CONNS=10 20 | DB_CONN_MAX_LIFETIME=300 21 | ``` 22 | 23 | ### Security Configuration 24 | ```bash 25 | # Encryption and Security 26 | ENCRYPTION_KEY=your-256-bit-encryption-key-here 27 | SESSION_SECRET=your-session-secret-key 28 | SESSION_TIMEOUT=3600 29 | CORS_ALLOWED_ORIGINS=https://yourdomain.com 30 | ``` 31 | 32 | ### Application Settings 33 | ```bash 34 | # Server Configuration 35 | PORT=8080 36 | GIN_MODE=release 37 | LOG_LEVEL=info 38 | UPLOAD_PATH=/app/uploads 39 | MAX_UPLOAD_SIZE=10485760 40 | 41 | # Feature Flags 42 | ENABLE_2FA=true 43 | ENABLE_AUDIT_LOG=true 44 | ENABLE_METRICS=true 45 | ``` 46 | 47 | ### Email Configuration (Optional) 48 | ```bash 49 | # SMTP Settings 50 | SMTP_HOST=smtp.yourdomain.com 51 | SMTP_PORT=587 52 | SMTP_USERNAME=noreply@yourdomain.com 53 | SMTP_PASSWORD=email_password 54 | SMTP_FROM=RentalCore 55 | ``` 56 | 57 | ## Application Configuration (config.json) 58 | 59 | ### UI Configuration 60 | ```json 61 | { 62 | "ui": { 63 | "theme": "dark", 64 | "company_name": "Your Company", 65 | "company_logo": "/static/images/logo.png", 66 | "timezone": "Europe/Berlin", 67 | "currency": "EUR", 68 | "date_format": "DD.MM.YYYY" 69 | } 70 | } 71 | ``` 72 | 73 | ### Feature Configuration 74 | ```json 75 | { 76 | "features": { 77 | "analytics": true, 78 | "qr_codes": true, 79 | "pdf_export": true, 80 | "bulk_operations": true, 81 | "device_packages": true 82 | } 83 | } 84 | ``` 85 | 86 | ### Invoice Configuration 87 | ```json 88 | { 89 | "invoice": { 90 | "tax_rate": 19.0, 91 | "payment_terms": 30, 92 | "invoice_prefix": "INV", 93 | "footer_text": "Thank you for your business!" 94 | } 95 | } 96 | ``` 97 | 98 | ### Performance Settings 99 | ```json 100 | { 101 | "performance": { 102 | "cache_timeout": 300, 103 | "max_results_per_page": 50, 104 | "analytics_cache_hours": 24, 105 | "session_cleanup_interval": 3600 106 | } 107 | } 108 | ``` 109 | 110 | ## Docker Compose Configuration 111 | 112 | ### Basic Configuration 113 | ```yaml 114 | version: '3.8' 115 | 116 | services: 117 | rentalcore: 118 | image: nbt4/rentalcore:latest 119 | container_name: rentalcore 120 | ports: 121 | - "8080:8080" 122 | environment: 123 | - DB_HOST=your-db-host 124 | - DB_NAME=rentalcore 125 | - DB_USERNAME=rentalcore_user 126 | - DB_PASSWORD=secure_password 127 | volumes: 128 | - ./uploads:/app/uploads 129 | - ./logs:/app/logs 130 | - ./config.json:/app/config.json 131 | restart: unless-stopped 132 | ``` 133 | 134 | ### Production Configuration with Proxy 135 | ```yaml 136 | version: '3.8' 137 | 138 | services: 139 | rentalcore: 140 | image: nbt4/rentalcore:latest 141 | container_name: rentalcore 142 | env_file: 143 | - .env 144 | volumes: 145 | - uploads_data:/app/uploads 146 | - logs_data:/app/logs 147 | - ./config.json:/app/config.json 148 | - ./keys:/app/keys 149 | labels: 150 | - "traefik.enable=true" 151 | - "traefik.http.routers.rentalcore.rule=Host(`rental.yourdomain.com`)" 152 | - "traefik.http.services.rentalcore.loadbalancer.server.port=8080" 153 | - "traefik.http.routers.rentalcore.tls.certresolver=letsencrypt" 154 | restart: unless-stopped 155 | healthcheck: 156 | test: ["CMD", "curl", "-f", "http://localhost:8080/health"] 157 | interval: 30s 158 | timeout: 10s 159 | retries: 3 160 | 161 | volumes: 162 | uploads_data: 163 | logs_data: 164 | ``` 165 | 166 | ## Configuration Validation 167 | 168 | ### Environment Validation 169 | RentalCore validates all environment variables on startup and will fail to start with clear error messages if required variables are missing. 170 | 171 | ### Configuration File Validation 172 | The application validates the config.json file structure and provides default values for missing optional settings. 173 | 174 | ## Security Considerations 175 | 176 | ### Credential Management 177 | - Never commit actual credentials to version control 178 | - Use strong, randomly generated passwords 179 | - Rotate encryption keys regularly 180 | - Use environment-specific configurations 181 | 182 | ### File Permissions 183 | - Ensure config files have appropriate permissions (600 or 644) 184 | - Protect sensitive directories from public access 185 | - Use dedicated service account for running the application -------------------------------------------------------------------------------- /docs/DOCKER_QUICK_START.md: -------------------------------------------------------------------------------- 1 | # RentalCore Docker Quick Start 2 | 3 | ## 🚀 Get Running in 5 Minutes 4 | 5 | This guide gets RentalCore running quickly for development and testing. For production deployment, see the [Docker Deployment Guide](DOCKER_DEPLOYMENT.md). 6 | 7 | ## Prerequisites 8 | 9 | - Docker Engine 20.10+ 10 | - Docker Compose 2.0+ 11 | - External MySQL database (or use Docker MySQL for testing) 12 | 13 | ## Option 1: With External Database (Recommended) 14 | 15 | ### Step 1: Download Configuration 16 | ```bash 17 | # Create project directory 18 | mkdir rentalcore && cd rentalcore 19 | 20 | # Download files 21 | curl -O https://github.com/nbt4/rentalcore/raw/main/docker-compose.example.yml 22 | curl -O https://github.com/nbt4/rentalcore/raw/main/.env.example 23 | curl -O https://github.com/nbt4/rentalcore/raw/main/config.json.example 24 | 25 | # Rename to active files 26 | mv docker-compose.example.yml docker-compose.yml 27 | mv .env.example .env 28 | mv config.json.example config.json 29 | ``` 30 | 31 | ### Step 2: Configure Database 32 | Edit `.env` file: 33 | ```bash 34 | nano .env 35 | ``` 36 | 37 | Update these values: 38 | ```bash 39 | DB_HOST=your-database-host.com 40 | DB_NAME=rentalcore 41 | DB_USERNAME=your_db_user 42 | DB_PASSWORD=your_secure_password 43 | ``` 44 | 45 | ### Step 3: Setup Database 46 | ```bash 47 | # Download and import database schema 48 | curl -O https://github.com/nbt4/rentalcore/raw/main/database/rentalcore_setup.sql 49 | mysql -h your-host -u your-user -p your-database < rentalcore_setup.sql 50 | ``` 51 | 52 | ### Step 4: Launch Application 53 | ```bash 54 | # Start RentalCore 55 | docker-compose up -d 56 | 57 | # Check status 58 | docker-compose ps 59 | 60 | # View logs 61 | docker-compose logs -f rentalcore 62 | ``` 63 | 64 | ### Step 5: Access Application 65 | - **URL**: http://localhost:8080 66 | - **Username**: admin 67 | - **Password**: admin123 68 | - **⚠️ Change password immediately!** 69 | 70 | --- 71 | 72 | ## Option 2: With Docker MySQL (Testing Only) 73 | 74 | ### Step 1: Complete Docker Setup 75 | ```bash 76 | mkdir rentalcore && cd rentalcore 77 | 78 | # Create docker-compose.yml 79 | cat > docker-compose.yml << 'EOF' 80 | version: '3.8' 81 | 82 | services: 83 | mysql: 84 | image: mysql:8.0 85 | container_name: rentalcore-mysql 86 | restart: unless-stopped 87 | environment: 88 | MYSQL_DATABASE: rentalcore 89 | MYSQL_USER: rentalcore_user 90 | MYSQL_PASSWORD: demo_password 91 | MYSQL_ROOT_PASSWORD: root_password 92 | volumes: 93 | - mysql_data:/var/lib/mysql 94 | ports: 95 | - "127.0.0.1:3306:3306" 96 | healthcheck: 97 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 98 | timeout: 20s 99 | retries: 10 100 | 101 | rentalcore: 102 | image: nbt4/rentalcore:latest 103 | container_name: rentalcore 104 | restart: unless-stopped 105 | environment: 106 | - DB_HOST=mysql 107 | - DB_NAME=rentalcore 108 | - DB_USERNAME=rentalcore_user 109 | - DB_PASSWORD=demo_password 110 | - GIN_MODE=release 111 | ports: 112 | - "8080:8080" 113 | volumes: 114 | - ./uploads:/app/uploads 115 | - ./logs:/app/logs 116 | depends_on: 117 | mysql: 118 | condition: service_healthy 119 | healthcheck: 120 | test: ["CMD", "curl", "-f", "http://localhost:8080/health"] 121 | interval: 30s 122 | timeout: 10s 123 | retries: 3 124 | 125 | volumes: 126 | mysql_data: 127 | EOF 128 | ``` 129 | 130 | ### Step 2: Launch Everything 131 | ```bash 132 | # Create directories 133 | mkdir -p uploads logs 134 | 135 | # Start all services 136 | docker-compose up -d 137 | 138 | # Wait for services to be ready 139 | sleep 30 140 | 141 | # Import database schema 142 | curl -o rentalcore_setup.sql https://github.com/nbt4/rentalcore/raw/main/database/rentalcore_setup.sql 143 | docker exec -i rentalcore-mysql mysql -u rentalcore_user -pdemo_password rentalcore < rentalcore_setup.sql 144 | ``` 145 | 146 | ### Step 3: Access Application 147 | - **URL**: http://localhost:8080 148 | - **Username**: admin 149 | - **Password**: admin123 150 | 151 | --- 152 | 153 | ## ✅ Verification Steps 154 | 155 | ### 1. Health Check 156 | ```bash 157 | curl http://localhost:8080/health 158 | # Expected: {"status":"ok","database":"connected"} 159 | ``` 160 | 161 | ### 2. Test Login 162 | 1. Open http://localhost:8080 163 | 2. Login with admin/admin123 164 | 3. Should see dashboard with sample data 165 | 166 | ### 3. Check Data 167 | Navigate to: 168 | - **Customers**: Should see 5 sample customers 169 | - **Devices**: Should see 10 sample devices 170 | - **Jobs**: Should see 5 sample jobs 171 | - **Analytics**: Should show revenue and equipment data 172 | 173 | --- 174 | 175 | ## 🔧 Quick Commands 176 | 177 | ### View Logs 178 | ```bash 179 | # All logs 180 | docker-compose logs -f 181 | 182 | # Just RentalCore 183 | docker-compose logs -f rentalcore 184 | 185 | # Just MySQL 186 | docker-compose logs -f mysql 187 | ``` 188 | 189 | ### Restart Services 190 | ```bash 191 | # Restart everything 192 | docker-compose restart 193 | 194 | # Restart just RentalCore 195 | docker-compose restart rentalcore 196 | ``` 197 | 198 | ### Update RentalCore 199 | ```bash 200 | # Pull latest version 201 | docker pull nbt4/rentalcore:latest 202 | 203 | # Recreate container with new image 204 | docker-compose up -d rentalcore 205 | ``` 206 | 207 | ### Stop Everything 208 | ```bash 209 | # Stop services (data preserved) 210 | docker-compose stop 211 | 212 | # Stop and remove containers 213 | docker-compose down 214 | 215 | # Stop and remove everything including data 216 | docker-compose down -v 217 | ``` 218 | 219 | --- 220 | 221 | ## 🚨 Important Security Notes 222 | 223 | ### For Production Use: 224 | 1. **Change default password** immediately after first login 225 | 2. **Use external database** with secure credentials 226 | 3. **Enable HTTPS** with proper SSL certificates 227 | 4. **Configure firewall** to restrict access 228 | 5. **Set up regular backups** 229 | 230 | ### Default Credentials to Change: 231 | - **Application**: admin/admin123 232 | - **Database** (if using Docker MySQL): root/root_password 233 | 234 | --- 235 | 236 | ## 📁 Directory Structure After Setup 237 | ``` 238 | rentalcore/ 239 | ├── docker-compose.yml # Docker services configuration 240 | ├── .env # Environment variables (if using Option 1) 241 | ├── config.json # Application configuration (if using Option 1) 242 | ├── uploads/ # File uploads (created automatically) 243 | ├── logs/ # Application logs (created automatically) 244 | └── rentalcore_setup.sql # Database schema (downloaded) 245 | ``` 246 | 247 | --- 248 | 249 | ## 🆘 Troubleshooting 250 | 251 | ### Container Won't Start 252 | ```bash 253 | # Check logs for errors 254 | docker-compose logs rentalcore 255 | 256 | # Common fixes: 257 | # 1. Ensure port 8080 is available 258 | # 2. Check database connectivity 259 | # 3. Verify environment variables 260 | ``` 261 | 262 | ### Database Connection Failed 263 | ```bash 264 | # Test database connectivity 265 | docker exec rentalcore ping mysql # For Docker MySQL 266 | # OR 267 | docker exec rentalcore ping your-external-db-host 268 | 269 | # Check database credentials in logs 270 | docker-compose logs rentalcore | grep -i database 271 | ``` 272 | 273 | ### Can't Access Web Interface 274 | ```bash 275 | # Check if service is running 276 | docker-compose ps 277 | 278 | # Check port mapping 279 | netstat -tulpn | grep :8080 280 | 281 | # Test direct connection 282 | curl http://localhost:8080/health 283 | ``` 284 | 285 | ### Need More Help? 286 | - Check [Troubleshooting Guide](TROUBLESHOOTING.md) 287 | - View [Full Deployment Guide](DOCKER_DEPLOYMENT.md) 288 | - Open issue on [GitHub](https://github.com/nbt4/rentalcore/issues) 289 | 290 | --- 291 | 292 | **🎉 You're ready to start managing equipment rentals with RentalCore!** -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # RentalCore Security Guide 2 | 3 | ## Overview 4 | RentalCore implements enterprise-grade security features to protect your rental management data. 5 | 6 | ## Authentication & Authorization 7 | 8 | ### Multi-Factor Authentication (2FA) 9 | - WebAuthn support for passwordless authentication 10 | - Time-based One-Time Password (TOTP) support 11 | - SMS verification for additional security 12 | 13 | ### Role-Based Access Control (RBAC) 14 | - **Admin**: Full system access 15 | - **Manager**: Job and device management 16 | - **User**: Limited read access 17 | 18 | ## Data Protection 19 | 20 | ### Encryption 21 | - AES-256 encryption for sensitive data 22 | - TLS 1.3 for data in transit 23 | - Encrypted database credentials 24 | 25 | ### Password Security 26 | - Minimum 8 characters 27 | - Must include uppercase, lowercase, number, special character 28 | - Password hashing with bcrypt 29 | - Session timeout after 1 hour of inactivity 30 | 31 | ## Security Features 32 | 33 | ### Input Validation 34 | - SQL injection prevention 35 | - XSS protection 36 | - CSRF tokens 37 | - Input sanitization 38 | 39 | ### Network Security 40 | - HTTPS/TLS termination 41 | - CORS protection 42 | - Rate limiting 43 | - IP whitelisting support 44 | 45 | ## Compliance 46 | 47 | ### GDPR Features 48 | - Data retention policies 49 | - Right to be forgotten 50 | - Data export functionality 51 | - Privacy controls 52 | 53 | ### Audit Logging 54 | - All user actions logged 55 | - Failed login attempts tracked 56 | - Data changes audited 57 | - Security event monitoring 58 | 59 | ## Best Practices 60 | 61 | 1. **Change Default Credentials**: Always change the default admin password 62 | 2. **Use Strong Passwords**: Enforce password complexity 63 | 3. **Regular Updates**: Keep Docker images updated 64 | 4. **Secure Headers**: Implement security headers in reverse proxy 65 | 5. **Database Security**: Use dedicated database user with minimal privileges 66 | 6. **Backup Encryption**: Encrypt all backup files 67 | 68 | ## Security Checklist 69 | 70 | - [ ] Changed default admin password 71 | - [ ] Configured HTTPS/TLS 72 | - [ ] Set up firewall rules 73 | - [ ] Configured secure headers 74 | - [ ] Enabled audit logging 75 | - [ ] Set up monitoring alerts 76 | - [ ] Configured backup encryption -------------------------------------------------------------------------------- /docs/TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # RentalCore Troubleshooting Guide 2 | 3 | ## Common Issues and Solutions 4 | 5 | ### Installation and Setup Issues 6 | 7 | #### Database Connection Failed 8 | **Problem**: Application fails to connect to database 9 | **Solutions**: 10 | 1. Verify database credentials in `.env` file 11 | 2. Ensure database server is running and accessible 12 | 3. Check firewall settings on database host 13 | 4. Verify database name exists and user has proper permissions 14 | 5. Test connection manually: 15 | ```bash 16 | mysql -h your-host -u username -p database_name 17 | ``` 18 | 19 | #### Docker Container Won't Start 20 | **Problem**: RentalCore container fails to start 21 | **Solutions**: 22 | 1. Check Docker logs: 23 | ```bash 24 | docker-compose logs rentalcore 25 | ``` 26 | 2. Verify environment variables are set correctly 27 | 3. Ensure ports are not already in use: 28 | ```bash 29 | netstat -tulpn | grep :8080 30 | ``` 31 | 4. Check file permissions on mounted volumes 32 | 5. Verify Docker image is latest version: 33 | ```bash 34 | docker pull nbt4/rentalcore:latest 35 | ``` 36 | 37 | ### Application Issues 38 | 39 | #### Analytics Page Not Loading 40 | **Problem**: Analytics dashboard shows no data or fails to load 41 | **Solutions**: 42 | 1. Check if you have sufficient data for the selected time period 43 | 2. Clear browser cache and reload the page 44 | 3. Check browser console for JavaScript errors 45 | 4. Verify database has completed jobs with revenue data 46 | 5. Check application logs for database query errors 47 | 48 | #### Dropdowns Not Working 49 | **Problem**: Dropdown menus not responding or showing options 50 | **Solutions**: 51 | 1. Clear browser cache and cookies 52 | 2. Disable browser extensions that might interfere 53 | 3. Check browser console for JavaScript errors 54 | 4. Verify page is fully loaded before interacting 55 | 5. Try different browser or incognito mode 56 | 57 | #### PDF Export Issues 58 | **Problem**: PDF exports showing corrupted characters or failing 59 | **Solutions**: 60 | 1. Ensure UTF-8 encoding is properly configured 61 | 2. Check for special characters in data (€ symbols, umlauts) 62 | 3. Verify sufficient system memory for PDF generation 63 | 4. Check application logs for PDF generation errors 64 | 5. Try exporting smaller date ranges 65 | 66 | ### Performance Issues 67 | 68 | #### Slow Loading Times 69 | **Problem**: Application pages load slowly 70 | **Solutions**: 71 | 1. Check database performance and optimize queries 72 | 2. Verify adequate system resources (CPU, RAM) 73 | 3. Check network connectivity between application and database 74 | 4. Clear analytics cache to refresh stale data 75 | 5. Consider database indexing optimization 76 | 77 | #### High Memory Usage 78 | **Problem**: Docker container using excessive memory 79 | **Solutions**: 80 | 1. Monitor database connection pool size 81 | 2. Check for memory leaks in application logs 82 | 3. Restart the container to clear memory: 83 | ```bash 84 | docker-compose restart rentalcore 85 | ``` 86 | 4. Reduce concurrent database connections 87 | 5. Consider upgrading server resources 88 | 89 | ### User Access Issues 90 | 91 | #### Login Failed 92 | **Problem**: Unable to login with correct credentials 93 | **Solutions**: 94 | 1. Verify username and password are correct 95 | 2. Check if account is active and not locked 96 | 3. Clear browser cookies and cache 97 | 4. Verify database connectivity 98 | 5. Check for typos in username (case sensitive) 99 | 6. Reset password if necessary 100 | 101 | #### Permission Denied 102 | **Problem**: User cannot access certain features 103 | **Solutions**: 104 | 1. Verify user role and permissions 105 | 2. Check if feature is enabled in configuration 106 | 3. Ensure user is logged in with correct account 107 | 4. Contact administrator to verify role assignments 108 | 5. Check application logs for authorization errors 109 | 110 | ### Data Issues 111 | 112 | #### Missing Equipment 113 | **Problem**: Devices not showing in equipment lists 114 | **Solutions**: 115 | 1. Check device status filters (available, maintenance, etc.) 116 | 2. Verify device is not assigned to active job 117 | 3. Check if device was accidentally deleted 118 | 4. Search by device ID or serial number 119 | 5. Check database for data consistency 120 | 121 | #### Revenue Data Incorrect 122 | **Problem**: Analytics showing wrong revenue numbers 123 | **Solutions**: 124 | 1. Verify job completion dates are set correctly 125 | 2. Check if both `revenue` and `final_revenue` fields are populated 126 | 3. Ensure jobs have proper status (completed vs. active) 127 | 4. Verify currency settings in configuration 128 | 5. Check for duplicate job entries 129 | 130 | #### Customer Information Missing 131 | **Problem**: Customer data not displaying correctly 132 | **Solutions**: 133 | 1. Check if customer record exists in database 134 | 2. Verify customer is not accidentally archived 135 | 3. Check for special characters in customer names 136 | 4. Ensure proper data encoding (UTF-8) 137 | 5. Check import process if data was bulk imported 138 | 139 | ### System Administration Issues 140 | 141 | #### Backup Failures 142 | **Problem**: Automated backups not working 143 | **Solutions**: 144 | 1. Check disk space on backup destination 145 | 2. Verify backup script permissions 146 | 3. Check database credentials for backup user 147 | 4. Ensure backup directory is writable 148 | 5. Test backup process manually 149 | 150 | #### SSL/HTTPS Issues 151 | **Problem**: SSL certificate errors or HTTPS not working 152 | **Solutions**: 153 | 1. Verify SSL certificate is valid and not expired 154 | 2. Check reverse proxy configuration (Traefik, Nginx) 155 | 3. Ensure proper DNS configuration 156 | 4. Verify certificate chain is complete 157 | 5. Check firewall settings for HTTPS traffic 158 | 159 | ### Diagnostic Commands 160 | 161 | #### Health Check 162 | ```bash 163 | # Check application health 164 | curl http://localhost:8080/health 165 | 166 | # Check Docker container status 167 | docker-compose ps 168 | 169 | # View recent logs 170 | docker-compose logs --tail=50 rentalcore 171 | ``` 172 | 173 | #### Database Diagnostics 174 | ```bash 175 | # Test database connection 176 | docker exec -it mysql-container mysql -u root -p 177 | 178 | # Check database size 179 | SELECT 180 | table_schema AS "Database", 181 | ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS "Size (MB)" 182 | FROM information_schema.tables 183 | GROUP BY table_schema; 184 | 185 | # Verify sample data 186 | USE rentalcore; 187 | SELECT COUNT(*) FROM customers; 188 | SELECT COUNT(*) FROM devices; 189 | SELECT COUNT(*) FROM jobs; 190 | ``` 191 | 192 | #### Performance Monitoring 193 | ```bash 194 | # Monitor resource usage 195 | docker stats rentalcore 196 | 197 | # Check system load 198 | htop 199 | 200 | # Monitor disk usage 201 | df -h 202 | ``` 203 | 204 | ## Getting Additional Help 205 | 206 | ### Log Analysis 207 | Always check the application logs when troubleshooting: 208 | ```bash 209 | # Application logs 210 | docker-compose logs -f rentalcore 211 | 212 | # System logs 213 | journalctl -u docker 214 | ``` 215 | 216 | ### Support Resources 217 | 1. **GitHub Issues**: https://github.com/nbt4/rentalcore/issues 218 | 2. **Documentation**: Check all files in the `docs/` folder 219 | 3. **Community Support**: GitHub Discussions 220 | 4. **Database Setup**: Refer to `docs/DATABASE_SETUP.md` 221 | 222 | ### Reporting Issues 223 | When reporting issues, please include: 224 | 1. RentalCore version (Docker image tag) 225 | 2. Operating system and Docker version 226 | 3. Relevant log excerpts 227 | 4. Steps to reproduce the problem 228 | 5. Expected vs actual behavior 229 | 230 | ### Emergency Recovery 231 | If the system is completely unresponsive: 232 | 1. Stop all containers: `docker-compose down` 233 | 2. Check logs: `docker-compose logs rentalcore` 234 | 3. Restore from backup if necessary 235 | 4. Start with minimal configuration to isolate issues 236 | 5. Contact support with detailed error information -------------------------------------------------------------------------------- /docs/USER_GUIDE.md: -------------------------------------------------------------------------------- 1 | # RentalCore User Guide 2 | 3 | ## Getting Started 4 | 5 | ### First Login 6 | 1. Navigate to your RentalCore installation (e.g., http://localhost:8080) 7 | 2. Login with default credentials: `admin` / `admin123` 8 | 3. **Important**: Change the default password immediately after first login 9 | 10 | ## Main Features 11 | 12 | ### Dashboard Overview 13 | The dashboard provides a quick overview of: 14 | - Active rentals and upcoming returns 15 | - Equipment availability status 16 | - Recent customer activity 17 | - Revenue summary 18 | 19 | ### Customer Management 20 | 21 | #### Adding Customers 22 | 1. Navigate to "Customers" in the main menu 23 | 2. Click "Add New Customer" 24 | 3. Fill in customer details: 25 | - Personal information (name, email, phone) 26 | - Company details (if applicable) 27 | - Address information 28 | 4. Click "Save Customer" 29 | 30 | #### Managing Customers 31 | - **Search**: Use the search bar to find customers by name, company, or email 32 | - **Edit**: Click the edit icon to modify customer information 33 | - **View History**: Click on a customer to see their rental history 34 | 35 | ### Equipment Management 36 | 37 | #### Device Categories and Products 38 | - **Categories**: Organize equipment into logical groups (Audio, Lighting, etc.) 39 | - **Products**: Define equipment types with daily rental rates 40 | - **Devices**: Individual equipment items with serial numbers and status 41 | 42 | #### Adding Equipment 43 | 1. Navigate to "Devices" in the main menu 44 | 2. Click "Add New Device" 45 | 3. Select product type and enter device details: 46 | - Device ID (unique identifier) 47 | - Serial number 48 | - Purchase information 49 | - Condition notes 50 | 4. Click "Save Device" 51 | 52 | #### Device Status Management 53 | - **Available**: Ready for rental 54 | - **Checked Out**: Currently on a job 55 | - **Maintenance**: Under repair or servicing 56 | - **Retired**: No longer available for rental 57 | 58 | ### Job Management 59 | 60 | #### Creating Rental Jobs 61 | 1. Navigate to "Jobs" in the main menu 62 | 2. Click "Create New Job" 63 | 3. Fill in job details: 64 | - Select customer 65 | - Set start and end dates 66 | - Add job description and location 67 | - Assign equipment to the job 68 | 4. Click "Create Job" 69 | 70 | #### Job Lifecycle 71 | 1. **Planning**: Initial job creation and equipment assignment 72 | 2. **Active**: Equipment deployed for the rental period 73 | 3. **Completed**: Job finished and equipment returned 74 | 4. **Cancelled**: Job cancelled before completion 75 | 76 | #### Equipment Assignment 77 | - Use bulk scanning to quickly assign multiple devices 78 | - Generate QR codes for easy device identification 79 | - Track equipment condition before and after rental 80 | 81 | ### Analytics and Reporting 82 | 83 | #### Analytics Dashboard 84 | Access comprehensive analytics including: 85 | - Revenue trends over different time periods 86 | - Equipment utilization rates 87 | - Customer activity analysis 88 | - Top performing equipment and customers 89 | 90 | #### Individual Device Analytics 91 | Click on any device to view: 92 | - Revenue generated over time 93 | - Utilization statistics 94 | - Booking history 95 | - Performance metrics 96 | 97 | #### Exporting Data 98 | - **PDF Export**: Professional reports for presentations 99 | - **CSV Export**: Data for external analysis 100 | - **Invoice Generation**: Professional invoices for customers 101 | 102 | ### Search and Navigation 103 | 104 | #### Global Search 105 | Use the search bar in the top navigation to quickly find: 106 | - Customers by name, company, or email 107 | - Devices by ID or serial number 108 | - Jobs by description or customer 109 | - Any combination of the above 110 | 111 | #### Quick Actions 112 | - **Scan QR Code**: Use camera to quickly identify equipment 113 | - **Bulk Operations**: Perform actions on multiple items 114 | - **Quick Add**: Rapid creation of common items 115 | 116 | ## Best Practices 117 | 118 | ### Equipment Management 119 | 1. Always update device status when equipment is deployed or returned 120 | 2. Use descriptive device IDs and serial numbers 121 | 3. Regular maintenance scheduling to keep equipment in good condition 122 | 4. Keep purchase and condition information up to date 123 | 124 | ### Customer Relations 125 | 1. Maintain accurate customer contact information 126 | 2. Keep notes about customer preferences and requirements 127 | 3. Follow up on overdue returns promptly 128 | 4. Generate professional invoices for all completed jobs 129 | 130 | ### Data Management 131 | 1. Regular backups of important data 132 | 2. Keep job descriptions detailed for future reference 133 | 3. Use consistent naming conventions 134 | 4. Regular cleanup of old or irrelevant data 135 | 136 | ## Troubleshooting 137 | 138 | ### Common Issues 139 | 140 | #### Login Problems 141 | - Verify username and password 142 | - Clear browser cache and cookies 143 | - Check if account is active 144 | 145 | #### Equipment Not Showing 146 | - Check device status filter settings 147 | - Verify device is not already assigned to another job 148 | - Ensure device exists in the system 149 | 150 | #### Analytics Not Loading 151 | - Check if you have sufficient data for the selected time period 152 | - Try refreshing the page 153 | - Verify database connectivity 154 | 155 | ### Getting Help 156 | If you encounter issues not covered in this guide: 157 | 1. Check the troubleshooting documentation 158 | 2. Review application logs for error messages 159 | 3. Contact your system administrator 160 | 4. Submit an issue on GitHub for bugs or feature requests -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-barcode-webapp 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.11 6 | 7 | require ( 8 | github.com/boombuler/barcode v1.0.1 9 | github.com/gin-gonic/gin v1.9.1 10 | github.com/jung-kurt/gofpdf v1.16.2 11 | github.com/makiuchi-d/gozxing v0.1.1 12 | github.com/pquerna/otp v1.5.0 13 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 14 | github.com/spf13/cobra v1.9.1 15 | golang.org/x/crypto v0.40.0 16 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a 17 | gorm.io/driver/mysql v1.5.1 18 | gorm.io/gorm v1.25.4 19 | ) 20 | 21 | require ( 22 | github.com/bytedance/sonic v1.9.1 // indirect 23 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 25 | github.com/gin-contrib/sse v0.1.0 // indirect 26 | github.com/go-playground/locales v0.14.1 // indirect 27 | github.com/go-playground/universal-translator v0.18.1 // indirect 28 | github.com/go-playground/validator/v10 v10.14.0 // indirect 29 | github.com/go-sql-driver/mysql v1.7.1 // indirect 30 | github.com/goccy/go-json v0.10.2 // indirect 31 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 32 | github.com/jinzhu/inflection v1.0.0 // indirect 33 | github.com/jinzhu/now v1.1.5 // indirect 34 | github.com/json-iterator/go v1.1.12 // indirect 35 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 36 | github.com/kr/pretty v0.3.0 // indirect 37 | github.com/leodido/go-urn v1.2.4 // indirect 38 | github.com/mattn/go-isatty v0.0.19 // indirect 39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 40 | github.com/modern-go/reflect2 v1.0.2 // indirect 41 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 42 | github.com/rogpeppe/go-internal v1.8.0 // indirect 43 | github.com/spf13/pflag v1.0.6 // indirect 44 | github.com/stretchr/testify v1.10.0 // indirect 45 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 46 | github.com/ugorji/go/codec v1.2.11 // indirect 47 | golang.org/x/arch v0.3.0 // indirect 48 | golang.org/x/net v0.41.0 // indirect 49 | golang.org/x/sys v0.34.0 // indirect 50 | golang.org/x/text v0.27.0 // indirect 51 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 52 | google.golang.org/protobuf v1.30.0 // indirect 53 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 54 | gopkg.in/yaml.v3 v3.0.1 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /img/cables-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/cables-management.png -------------------------------------------------------------------------------- /img/case-devices-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/case-devices-modal.png -------------------------------------------------------------------------------- /img/cases-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/cases-management.png -------------------------------------------------------------------------------- /img/dashboard-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/dashboard-overview.png -------------------------------------------------------------------------------- /img/device-tree-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/device-tree-view.png -------------------------------------------------------------------------------- /img/devices-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/devices-management.png -------------------------------------------------------------------------------- /img/job-overview-barcode-scanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/job-overview-barcode-scanner.png -------------------------------------------------------------------------------- /img/job-scanning-interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/job-scanning-interface.png -------------------------------------------------------------------------------- /img/job-selection-scanning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/job-selection-scanning.png -------------------------------------------------------------------------------- /img/login-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/login-page.png -------------------------------------------------------------------------------- /img/products-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/img/products-management.png -------------------------------------------------------------------------------- /internal/database/migrations/001_performance_indexes.sql: -------------------------------------------------------------------------------- 1 | -- Critical Performance Indexes for Production 2 | -- Execute these indexes to resolve N+1 query problems and improve performance 3 | 4 | -- High Priority Indexes for Core Functionality 5 | CREATE INDEX IF NOT EXISTS idx_devices_productid ON devices(productID); 6 | CREATE INDEX IF NOT EXISTS idx_devices_status ON devices(status); 7 | CREATE INDEX IF NOT EXISTS idx_devices_deviceid ON devices(deviceID); 8 | 9 | -- Job-Device Relationship Optimization 10 | CREATE INDEX IF NOT EXISTS idx_jobdevices_deviceid ON jobdevices(deviceID); 11 | CREATE INDEX IF NOT EXISTS idx_jobdevices_jobid ON jobdevices(jobID); 12 | CREATE INDEX IF NOT EXISTS idx_jobdevices_composite ON jobdevices(deviceID, jobID); 13 | 14 | -- Jobs Performance Indexes 15 | CREATE INDEX IF NOT EXISTS idx_jobs_customerid ON jobs(customerID); 16 | CREATE INDEX IF NOT EXISTS idx_jobs_statusid ON jobs(statusID); 17 | CREATE INDEX IF NOT EXISTS idx_jobs_dates ON jobs(startDate, endDate); 18 | CREATE INDEX IF NOT EXISTS idx_jobs_customer_status ON jobs(customerID, statusID); 19 | 20 | -- Invoice Performance Indexes 21 | CREATE INDEX IF NOT EXISTS idx_invoices_customerid ON invoices(customer_id); 22 | CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status); 23 | CREATE INDEX IF NOT EXISTS idx_invoices_dates ON invoices(issue_date, due_date); 24 | CREATE INDEX IF NOT EXISTS idx_invoices_number ON invoices(invoice_number); 25 | 26 | -- Customer Relationship Indexes 27 | CREATE INDEX IF NOT EXISTS idx_customers_status ON customers(status); 28 | CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email); 29 | 30 | -- Search Optimization Indexes 31 | CREATE INDEX IF NOT EXISTS idx_devices_search ON devices(deviceID, serialnumber); 32 | CREATE INDEX IF NOT EXISTS idx_customers_search_company ON customers(companyname); 33 | CREATE INDEX IF NOT EXISTS idx_customers_search_name ON customers(firstname, lastname); 34 | 35 | -- Product and Category Indexes 36 | CREATE INDEX IF NOT EXISTS idx_products_categoryid ON products(categoryID); 37 | CREATE INDEX IF NOT EXISTS idx_products_status ON products(status); 38 | 39 | -- Composite Indexes for Complex Queries 40 | CREATE INDEX IF NOT EXISTS idx_devices_product_status ON devices(productID, status); 41 | CREATE INDEX IF NOT EXISTS idx_jobs_status_dates ON jobs(statusID, startDate, endDate); 42 | 43 | -- Transaction Performance 44 | CREATE INDEX IF NOT EXISTS idx_transactions_customerid ON transactions(customerID); 45 | CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(transactionDate); 46 | 47 | -- Session Management 48 | CREATE INDEX IF NOT EXISTS idx_sessions_userid ON sessions(user_id); 49 | CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); 50 | 51 | -- Company Settings 52 | CREATE INDEX IF NOT EXISTS idx_company_settings_updated ON company_settings(updated_at); 53 | 54 | -- Email Templates 55 | CREATE INDEX IF NOT EXISTS idx_email_templates_type ON email_templates(template_type); 56 | CREATE INDEX IF NOT EXISTS idx_email_templates_default ON email_templates(template_type, is_default); 57 | 58 | -- Invoice Templates 59 | CREATE INDEX IF NOT EXISTS idx_invoice_templates_default ON invoice_templates(is_default); 60 | CREATE INDEX IF NOT EXISTS idx_invoice_templates_active ON invoice_templates(is_active); 61 | 62 | -- Invoice Line Items 63 | CREATE INDEX IF NOT EXISTS idx_invoice_line_items_invoice ON invoice_line_items(invoice_id); 64 | CREATE INDEX IF NOT EXISTS idx_invoice_line_items_device ON invoice_line_items(device_id); 65 | 66 | -- Equipment Packages 67 | CREATE INDEX IF NOT EXISTS idx_equipment_packages_status ON equipment_packages(is_active); 68 | 69 | -- Cases 70 | CREATE INDEX IF NOT EXISTS idx_cases_status ON cases(status); 71 | CREATE INDEX IF NOT EXISTS idx_cases_customerid ON cases(customerID); 72 | 73 | -- Case Device Mappings 74 | CREATE INDEX IF NOT EXISTS idx_case_device_mappings_case ON case_device_mappings(caseID); 75 | CREATE INDEX IF NOT EXISTS idx_case_device_mappings_device ON case_device_mappings(deviceID); -------------------------------------------------------------------------------- /internal/handlers/barcode_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go-barcode-webapp/internal/repository" 7 | "go-barcode-webapp/internal/services" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type BarcodeHandler struct { 13 | barcodeService *services.BarcodeService 14 | deviceRepo *repository.DeviceRepository 15 | } 16 | 17 | func NewBarcodeHandler(barcodeService *services.BarcodeService, deviceRepo *repository.DeviceRepository) *BarcodeHandler { 18 | return &BarcodeHandler{ 19 | barcodeService: barcodeService, 20 | deviceRepo: deviceRepo, 21 | } 22 | } 23 | 24 | func (h *BarcodeHandler) GenerateDeviceQR(c *gin.Context) { 25 | serialNo := c.Param("serialNo") 26 | if serialNo == "" { 27 | c.JSON(http.StatusBadRequest, gin.H{"error": "Serial number is required"}) 28 | return 29 | } 30 | 31 | // Verify device exists 32 | _, err := h.deviceRepo.GetBySerialNo(serialNo) 33 | if err != nil { 34 | c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"}) 35 | return 36 | } 37 | 38 | qrBytes, err := h.barcodeService.GenerateDeviceQR(serialNo) 39 | if err != nil { 40 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 41 | return 42 | } 43 | 44 | c.Header("Content-Type", "image/png") 45 | c.Header("Content-Disposition", "inline; filename=device_"+serialNo+"_qr.png") 46 | c.Data(http.StatusOK, "image/png", qrBytes) 47 | } 48 | 49 | func (h *BarcodeHandler) GenerateDeviceBarcode(c *gin.Context) { 50 | serialNo := c.Param("serialNo") 51 | if serialNo == "" { 52 | c.JSON(http.StatusBadRequest, gin.H{"error": "Serial number is required"}) 53 | return 54 | } 55 | 56 | // Verify device exists 57 | _, err := h.deviceRepo.GetBySerialNo(serialNo) 58 | if err != nil { 59 | c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"}) 60 | return 61 | } 62 | 63 | barcodeBytes, err := h.barcodeService.GenerateDeviceBarcode(serialNo) 64 | if err != nil { 65 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 66 | return 67 | } 68 | 69 | c.Header("Content-Type", "image/png") 70 | c.Header("Content-Disposition", "inline; filename=device_"+serialNo+"_barcode.png") 71 | c.Data(http.StatusOK, "image/png", barcodeBytes) 72 | } -------------------------------------------------------------------------------- /internal/handlers/home_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go-barcode-webapp/internal/models" 7 | "go-barcode-webapp/internal/repository" 8 | 9 | "github.com/gin-gonic/gin" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type HomeHandler struct { 14 | jobRepo *repository.JobRepository 15 | deviceRepo *repository.DeviceRepository 16 | customerRepo *repository.CustomerRepository 17 | caseRepo *repository.CaseRepository 18 | db *gorm.DB 19 | } 20 | 21 | func NewHomeHandler(jobRepo *repository.JobRepository, deviceRepo *repository.DeviceRepository, customerRepo *repository.CustomerRepository, caseRepo *repository.CaseRepository, db *gorm.DB) *HomeHandler { 22 | return &HomeHandler{ 23 | jobRepo: jobRepo, 24 | deviceRepo: deviceRepo, 25 | customerRepo: customerRepo, 26 | caseRepo: caseRepo, 27 | db: db, 28 | } 29 | } 30 | 31 | func (h *HomeHandler) Dashboard(c *gin.Context) { 32 | user, _ := GetCurrentUser(c) 33 | 34 | // Get real counts from database using direct queries 35 | var totalJobs int64 36 | var activeJobs int64 37 | var totalDevices int64 38 | var totalCustomers int64 39 | var totalCases int64 40 | 41 | // Use the DB connection to count records 42 | h.db.Model(&models.Job{}).Count(&totalJobs) 43 | // Count active jobs by joining with status table to get actual status names 44 | h.db.Table("jobs j"). 45 | Joins("LEFT JOIN status s ON j.statusID = s.statusID"). 46 | Where("s.status NOT IN ('Completed', 'Cancelled', 'completed', 'cancelled', 'paid', 'On Hold')"). 47 | Count(&activeJobs) 48 | h.db.Model(&models.Device{}).Count(&totalDevices) 49 | h.db.Model(&models.Customer{}).Count(&totalCustomers) 50 | h.db.Model(&models.Case{}).Count(&totalCases) 51 | 52 | stats := gin.H{ 53 | "TotalJobs": totalJobs, 54 | "ActiveJobs": activeJobs, 55 | "TotalDevices": totalDevices, 56 | "TotalCustomers": totalCustomers, 57 | "TotalCases": totalCases, 58 | } 59 | 60 | // Get recent jobs (limit to 5 for performance) 61 | recentJobs, _ := h.jobRepo.List(&models.FilterParams{ 62 | Limit: 5, 63 | }) 64 | 65 | c.HTML(http.StatusOK, "home.html", gin.H{ 66 | "title": "Home", 67 | "user": user, 68 | "stats": stats, 69 | "recentJobs": recentJobs, 70 | "currentPage": "home", 71 | }) 72 | } -------------------------------------------------------------------------------- /internal/handlers/status_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go-barcode-webapp/internal/repository" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type StatusHandler struct { 12 | statusRepo *repository.StatusRepository 13 | } 14 | 15 | func NewStatusHandler(statusRepo *repository.StatusRepository) *StatusHandler { 16 | return &StatusHandler{statusRepo: statusRepo} 17 | } 18 | 19 | func (h *StatusHandler) ListStatuses(c *gin.Context) { 20 | statuses, err := h.statusRepo.List() 21 | if err != nil { 22 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 23 | return 24 | } 25 | 26 | c.JSON(http.StatusOK, gin.H{"statuses": statuses}) 27 | } 28 | 29 | -------------------------------------------------------------------------------- /internal/handlers/types.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "go-barcode-webapp/internal/models" 4 | 5 | // ProductGroup represents a group of devices organized by product 6 | type ProductGroup struct { 7 | Product *models.Product `json:"product"` 8 | Devices []models.JobDevice `json:"devices"` 9 | Count int `json:"count"` 10 | TotalValue float64 `json:"total_value"` 11 | } -------------------------------------------------------------------------------- /internal/repository/cable_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "log" 5 | "go-barcode-webapp/internal/models" 6 | ) 7 | 8 | type CableRepository struct { 9 | db *Database 10 | } 11 | 12 | func NewCableRepository(db *Database) *CableRepository { 13 | return &CableRepository{db: db} 14 | } 15 | 16 | func (r *CableRepository) Create(cable *models.Cable) error { 17 | return r.db.Create(cable).Error 18 | } 19 | 20 | func (r *CableRepository) GetByID(id int) (*models.Cable, error) { 21 | var cable models.Cable 22 | err := r.db.Preload("Connector1Info").Preload("Connector2Info").Preload("TypeInfo").First(&cable, id).Error 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &cable, nil 27 | } 28 | 29 | func (r *CableRepository) Update(cable *models.Cable) error { 30 | return r.db.Save(cable).Error 31 | } 32 | 33 | func (r *CableRepository) Delete(id int) error { 34 | return r.db.Delete(&models.Cable{}, id).Error 35 | } 36 | 37 | func (r *CableRepository) List(params *models.FilterParams) ([]models.Cable, error) { 38 | var cables []models.Cable 39 | 40 | query := r.db.Model(&models.Cable{}). 41 | Preload("Connector1Info"). 42 | Preload("Connector2Info"). 43 | Preload("TypeInfo") 44 | 45 | if params.SearchTerm != "" { 46 | searchPattern := "%" + params.SearchTerm + "%" 47 | query = query.Where("name LIKE ?", searchPattern) 48 | } 49 | 50 | if params.Limit > 0 { 51 | query = query.Limit(params.Limit) 52 | } 53 | if params.Offset > 0 { 54 | query = query.Offset(params.Offset) 55 | } 56 | 57 | query = query.Order("name ASC") 58 | 59 | err := query.Find(&cables).Error 60 | return cables, err 61 | } 62 | 63 | // ListGrouped returns cables grouped by specifications with count 64 | func (r *CableRepository) ListGrouped(params *models.FilterParams) ([]models.CableGroup, error) { 65 | var groups []models.CableGroup 66 | 67 | // Build the base query for grouping 68 | query := r.db.Model(&models.Cable{}). 69 | Select("typ as type, connector1, connector2, length, mm2, name, COUNT(*) as count"). 70 | Group("typ, connector1, connector2, length, mm2, name"). 71 | Order("name ASC") 72 | 73 | if params.SearchTerm != "" { 74 | searchPattern := "%" + params.SearchTerm + "%" 75 | query = query.Where("name LIKE ?", searchPattern) 76 | } 77 | 78 | if params.Limit > 0 { 79 | query = query.Limit(params.Limit) 80 | } 81 | if params.Offset > 0 { 82 | query = query.Offset(params.Offset) 83 | } 84 | 85 | // Execute the grouping query 86 | err := query.Find(&groups).Error 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | // Load relationship data for each group 92 | for i := range groups { 93 | // Load connector info 94 | if groups[i].Connector1 > 0 { 95 | var connector1 models.CableConnector 96 | if err := r.db.First(&connector1, groups[i].Connector1).Error; err == nil { 97 | groups[i].Connector1Info = &connector1 98 | } 99 | } 100 | 101 | if groups[i].Connector2 > 0 { 102 | var connector2 models.CableConnector 103 | if err := r.db.First(&connector2, groups[i].Connector2).Error; err == nil { 104 | groups[i].Connector2Info = &connector2 105 | } 106 | } 107 | 108 | // Load type info 109 | if groups[i].Type > 0 { 110 | var cableType models.CableType 111 | if err := r.db.First(&cableType, groups[i].Type).Error; err == nil { 112 | groups[i].TypeInfo = &cableType 113 | } 114 | } 115 | 116 | // Get sample cable IDs for this group 117 | var cableIDs []int 118 | whereClause := "typ = ? AND connector1 = ? AND connector2 = ? AND length = ? AND COALESCE(name, '') = COALESCE(?, '')" 119 | args := []interface{}{groups[i].Type, groups[i].Connector1, groups[i].Connector2, groups[i].Length, groups[i].Name} 120 | 121 | if groups[i].MM2 != nil { 122 | whereClause += " AND mm2 = ?" 123 | args = append(args, *groups[i].MM2) 124 | } else { 125 | whereClause += " AND mm2 IS NULL" 126 | } 127 | 128 | r.db.Model(&models.Cable{}). 129 | Select("cableID"). 130 | Where(whereClause, args...). 131 | Pluck("cableID", &cableIDs) 132 | groups[i].CableIDs = cableIDs 133 | } 134 | 135 | return groups, nil 136 | } 137 | 138 | func (r *CableRepository) GetTotalCount() (int, error) { 139 | var count int64 140 | err := r.db.Model(&models.Cable{}).Count(&count).Error 141 | return int(count), err 142 | } 143 | 144 | // Get all cable types for forms 145 | func (r *CableRepository) GetAllCableTypes() ([]models.CableType, error) { 146 | var types []models.CableType 147 | err := r.db.Order("name ASC").Find(&types).Error 148 | if err != nil { 149 | log.Printf("❌ GetAllCableTypes error: %v", err) 150 | return nil, err 151 | } 152 | return types, nil 153 | } 154 | 155 | // Get all cable connectors for forms 156 | func (r *CableRepository) GetAllCableConnectors() ([]models.CableConnector, error) { 157 | var connectors []models.CableConnector 158 | err := r.db.Order("name ASC").Find(&connectors).Error 159 | if err != nil { 160 | log.Printf("❌ GetAllCableConnectors error: %v", err) 161 | return nil, err 162 | } 163 | return connectors, nil 164 | } -------------------------------------------------------------------------------- /internal/repository/customer_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "go-barcode-webapp/internal/models" 6 | ) 7 | 8 | type CustomerRepository struct { 9 | db *Database 10 | } 11 | 12 | func NewCustomerRepository(db *Database) *CustomerRepository { 13 | return &CustomerRepository{db: db} 14 | } 15 | 16 | func (r *CustomerRepository) Create(customer *models.Customer) error { 17 | fmt.Printf("🔧 DEBUG CustomerRepo.Create: Before DB operation, customer ID: %d\n", customer.CustomerID) 18 | result := r.db.Create(customer) 19 | fmt.Printf("🔧 DEBUG CustomerRepo.Create: After DB operation, customer ID: %d, Error: %v\n", customer.CustomerID, result.Error) 20 | fmt.Printf("🔧 DEBUG CustomerRepo.Create: Rows affected: %d\n", result.RowsAffected) 21 | return result.Error 22 | } 23 | 24 | func (r *CustomerRepository) GetByID(id uint) (*models.Customer, error) { 25 | var customer models.Customer 26 | err := r.db.First(&customer, id).Error 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &customer, nil 31 | } 32 | 33 | func (r *CustomerRepository) Update(customer *models.Customer) error { 34 | return r.db.Save(customer).Error 35 | } 36 | 37 | func (r *CustomerRepository) Delete(id uint) error { 38 | return r.db.Delete(&models.Customer{}, id).Error 39 | } 40 | 41 | func (r *CustomerRepository) List(params *models.FilterParams) ([]models.Customer, error) { 42 | var customers []models.Customer 43 | 44 | query := r.db.Model(&models.Customer{}) 45 | 46 | if params.SearchTerm != "" { 47 | searchPattern := "%" + params.SearchTerm + "%" 48 | query = query.Where("companyname LIKE ? OR firstname LIKE ? OR lastname LIKE ? OR email LIKE ?", searchPattern, searchPattern, searchPattern, searchPattern) 49 | } 50 | 51 | if params.Limit > 0 { 52 | query = query.Limit(params.Limit) 53 | } 54 | if params.Offset > 0 { 55 | query = query.Offset(params.Offset) 56 | } 57 | 58 | query = query.Order("companyname ASC") 59 | 60 | err := query.Find(&customers).Error 61 | return customers, err 62 | } -------------------------------------------------------------------------------- /internal/repository/database.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "go-barcode-webapp/internal/config" 9 | 10 | "gorm.io/driver/mysql" 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/logger" 13 | "gorm.io/gorm/schema" 14 | ) 15 | 16 | type Database struct { 17 | *gorm.DB 18 | } 19 | 20 | 21 | func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) { 22 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", 23 | cfg.Username, 24 | cfg.Password, 25 | cfg.Host, 26 | cfg.Port, 27 | cfg.Database, 28 | ) 29 | 30 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ 31 | Logger: logger.Default.LogMode(logger.Silent), 32 | DisableAutomaticPing: true, 33 | SkipDefaultTransaction: true, 34 | CreateBatchSize: 1000, 35 | NamingStrategy: schema.NamingStrategy{ 36 | SingularTable: true, 37 | }, 38 | }) 39 | if err != nil { 40 | return nil, fmt.Errorf("failed to connect to database: %w", err) 41 | } 42 | 43 | sqlDB, err := db.DB() 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to get sql.DB: %w", err) 46 | } 47 | 48 | sqlDB.SetMaxIdleConns(cfg.PoolSize / 2) 49 | sqlDB.SetMaxOpenConns(cfg.PoolSize) 50 | sqlDB.SetConnMaxLifetime(30 * time.Minute) 51 | sqlDB.SetConnMaxIdleTime(5 * time.Minute) 52 | 53 | // Basic database connection setup only - no schema operations 54 | 55 | log.Println("Database connection established successfully") 56 | return &Database{db}, nil 57 | } 58 | 59 | func (db *Database) Close() error { 60 | sqlDB, err := db.DB.DB() 61 | if err != nil { 62 | return err 63 | } 64 | return sqlDB.Close() 65 | } 66 | 67 | func (db *Database) Ping() error { 68 | sqlDB, err := db.DB.DB() 69 | if err != nil { 70 | return err 71 | } 72 | return sqlDB.Ping() 73 | } -------------------------------------------------------------------------------- /internal/repository/job_attachment_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "go-barcode-webapp/internal/models" 5 | "time" 6 | ) 7 | 8 | type JobAttachmentRepository struct { 9 | db *Database 10 | } 11 | 12 | func NewJobAttachmentRepository(db *Database) *JobAttachmentRepository { 13 | return &JobAttachmentRepository{db: db} 14 | } 15 | 16 | // Create creates a new job attachment 17 | func (r *JobAttachmentRepository) Create(attachment *models.JobAttachment) error { 18 | return r.db.Create(attachment).Error 19 | } 20 | 21 | // GetByID retrieves a job attachment by ID 22 | func (r *JobAttachmentRepository) GetByID(attachmentID uint) (*models.JobAttachment, error) { 23 | var attachment models.JobAttachment 24 | err := r.db.Where("attachment_id = ? AND is_active = ?", attachmentID, true). 25 | Preload("Job"). 26 | Preload("Uploader"). 27 | First(&attachment).Error 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &attachment, nil 32 | } 33 | 34 | // GetByJobID retrieves all attachments for a specific job 35 | func (r *JobAttachmentRepository) GetByJobID(jobID uint) ([]models.JobAttachment, error) { 36 | var attachments []models.JobAttachment 37 | err := r.db.Where("job_id = ? AND is_active = ?", jobID, true). 38 | Preload("Job"). 39 | Preload("Uploader"). 40 | Order("uploaded_at DESC"). 41 | Find(&attachments).Error 42 | return attachments, err 43 | } 44 | 45 | // Update updates a job attachment 46 | func (r *JobAttachmentRepository) Update(attachment *models.JobAttachment) error { 47 | return r.db.Save(attachment).Error 48 | } 49 | 50 | // Delete soft deletes a job attachment (sets is_active to false) 51 | func (r *JobAttachmentRepository) Delete(attachmentID uint) error { 52 | return r.db.Model(&models.JobAttachment{}). 53 | Where("attachment_id = ?", attachmentID). 54 | Update("is_active", false).Error 55 | } 56 | 57 | // HardDelete permanently deletes a job attachment 58 | func (r *JobAttachmentRepository) HardDelete(attachmentID uint) error { 59 | return r.db.Where("attachment_id = ?", attachmentID).Delete(&models.JobAttachment{}).Error 60 | } 61 | 62 | // GetByFilename retrieves a job attachment by filename 63 | func (r *JobAttachmentRepository) GetByFilename(filename string) (*models.JobAttachment, error) { 64 | var attachment models.JobAttachment 65 | err := r.db.Where("filename = ? AND is_active = ?", filename, true). 66 | Preload("Job"). 67 | Preload("Uploader"). 68 | First(&attachment).Error 69 | if err != nil { 70 | return nil, err 71 | } 72 | return &attachment, nil 73 | } 74 | 75 | // GetTotalSizeByJobID returns the total file size for all attachments of a job 76 | func (r *JobAttachmentRepository) GetTotalSizeByJobID(jobID uint) (int64, error) { 77 | var totalSize int64 78 | err := r.db.Model(&models.JobAttachment{}). 79 | Where("job_id = ? AND is_active = ?", jobID, true). 80 | Select("COALESCE(SUM(file_size), 0)"). 81 | Scan(&totalSize).Error 82 | return totalSize, err 83 | } 84 | 85 | // GetCountByJobID returns the number of attachments for a job 86 | func (r *JobAttachmentRepository) GetCountByJobID(jobID uint) (int64, error) { 87 | var count int64 88 | err := r.db.Model(&models.JobAttachment{}). 89 | Where("job_id = ? AND is_active = ?", jobID, true). 90 | Count(&count).Error 91 | return count, err 92 | } 93 | 94 | // GetRecentUploads returns recently uploaded attachments 95 | func (r *JobAttachmentRepository) GetRecentUploads(limit int) ([]models.JobAttachment, error) { 96 | var attachments []models.JobAttachment 97 | err := r.db.Where("is_active = ?", true). 98 | Preload("Job"). 99 | Preload("Uploader"). 100 | Order("uploaded_at DESC"). 101 | Limit(limit). 102 | Find(&attachments).Error 103 | return attachments, err 104 | } 105 | 106 | // GetByMimeType returns attachments filtered by MIME type 107 | func (r *JobAttachmentRepository) GetByMimeType(jobID uint, mimeType string) ([]models.JobAttachment, error) { 108 | var attachments []models.JobAttachment 109 | err := r.db.Where("job_id = ? AND mime_type LIKE ? AND is_active = ?", jobID, mimeType+"%", true). 110 | Preload("Job"). 111 | Preload("Uploader"). 112 | Order("uploaded_at DESC"). 113 | Find(&attachments).Error 114 | return attachments, err 115 | } 116 | 117 | // GetUploadedByUser returns attachments uploaded by a specific user 118 | func (r *JobAttachmentRepository) GetUploadedByUser(userID uint, limit int) ([]models.JobAttachment, error) { 119 | var attachments []models.JobAttachment 120 | err := r.db.Where("uploaded_by = ? AND is_active = ?", userID, true). 121 | Preload("Job"). 122 | Preload("Uploader"). 123 | Order("uploaded_at DESC"). 124 | Limit(limit). 125 | Find(&attachments).Error 126 | return attachments, err 127 | } 128 | 129 | // GetAttachmentsOlderThan returns attachments uploaded before a specific date 130 | func (r *JobAttachmentRepository) GetAttachmentsOlderThan(date time.Time) ([]models.JobAttachment, error) { 131 | var attachments []models.JobAttachment 132 | err := r.db.Where("uploaded_at < ? AND is_active = ?", date, true). 133 | Preload("Job"). 134 | Preload("Uploader"). 135 | Order("uploaded_at ASC"). 136 | Find(&attachments).Error 137 | return attachments, err 138 | } 139 | 140 | // SearchAttachments searches attachments by filename or description 141 | func (r *JobAttachmentRepository) SearchAttachments(jobID uint, searchTerm string) ([]models.JobAttachment, error) { 142 | var attachments []models.JobAttachment 143 | searchPattern := "%" + searchTerm + "%" 144 | 145 | err := r.db.Where("job_id = ? AND is_active = ? AND (original_filename LIKE ? OR description LIKE ?)", 146 | jobID, true, searchPattern, searchPattern). 147 | Preload("Job"). 148 | Preload("Uploader"). 149 | Order("uploaded_at DESC"). 150 | Find(&attachments).Error 151 | return attachments, err 152 | } -------------------------------------------------------------------------------- /internal/repository/job_category_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "go-barcode-webapp/internal/models" 5 | ) 6 | 7 | type JobCategoryRepository struct { 8 | db *Database 9 | } 10 | 11 | func NewJobCategoryRepository(db *Database) *JobCategoryRepository { 12 | return &JobCategoryRepository{db: db} 13 | } 14 | 15 | func (r *JobCategoryRepository) Create(category *models.JobCategory) error { 16 | return r.db.Create(category).Error 17 | } 18 | 19 | func (r *JobCategoryRepository) GetByID(id uint) (*models.JobCategory, error) { 20 | var category models.JobCategory 21 | err := r.db.First(&category, id).Error 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &category, nil 26 | } 27 | 28 | func (r *JobCategoryRepository) List() ([]models.JobCategory, error) { 29 | var categories []models.JobCategory 30 | err := r.db.Find(&categories).Error 31 | return categories, err 32 | } 33 | 34 | func (r *JobCategoryRepository) Update(category *models.JobCategory) error { 35 | return r.db.Save(category).Error 36 | } 37 | 38 | func (r *JobCategoryRepository) Delete(id uint) error { 39 | return r.db.Delete(&models.JobCategory{}, id).Error 40 | } -------------------------------------------------------------------------------- /internal/repository/job_repository_extension.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "go-barcode-webapp/internal/models" 6 | ) 7 | 8 | // FreeDevicesFromCompletedJobs removes device assignments from jobs with "cancelled" status 9 | // and sets the device status back to "free". Paid jobs should retain their device assignments for records. 10 | func (r *JobRepository) FreeDevicesFromCompletedJobs() error { 11 | // Find all jobs with "cancelled" status (NOT "paid" - paid jobs should keep device assignments) 12 | var cancelledJobs []models.Job 13 | err := r.db.Joins("JOIN status ON jobs.statusID = status.statusID"). 14 | Where("status.status = ?", "cancelled"). 15 | Find(&cancelledJobs).Error 16 | if err != nil { 17 | return fmt.Errorf("failed to find cancelled jobs: %v", err) 18 | } 19 | 20 | if len(cancelledJobs) == 0 { 21 | return nil // No cancelled jobs found 22 | } 23 | 24 | // Extract job IDs 25 | var jobIDs []uint 26 | for _, job := range cancelledJobs { 27 | jobIDs = append(jobIDs, job.JobID) 28 | } 29 | 30 | // Find all devices assigned to these jobs 31 | var jobDevices []models.JobDevice 32 | err = r.db.Where("jobID IN ?", jobIDs).Find(&jobDevices).Error 33 | if err != nil { 34 | return fmt.Errorf("failed to find devices in cancelled jobs: %v", err) 35 | } 36 | 37 | // Start transaction 38 | tx := r.db.Begin() 39 | if tx.Error != nil { 40 | return tx.Error 41 | } 42 | defer func() { 43 | if r := recover(); r != nil { 44 | tx.Rollback() 45 | } 46 | }() 47 | 48 | // Remove job device assignments 49 | if len(jobDevices) > 0 { 50 | err = tx.Where("jobID IN ?", jobIDs).Delete(&models.JobDevice{}).Error 51 | if err != nil { 52 | tx.Rollback() 53 | return fmt.Errorf("failed to remove job device assignments: %v", err) 54 | } 55 | 56 | // Set device status back to "free" 57 | var deviceIDs []string 58 | for _, jd := range jobDevices { 59 | deviceIDs = append(deviceIDs, jd.DeviceID) 60 | } 61 | 62 | err = tx.Model(&models.Device{}). 63 | Where("deviceID IN ?", deviceIDs). 64 | Update("status", "free").Error 65 | if err != nil { 66 | tx.Rollback() 67 | return fmt.Errorf("failed to update device status: %v", err) 68 | } 69 | } 70 | 71 | // Commit transaction 72 | err = tx.Commit().Error 73 | if err != nil { 74 | return fmt.Errorf("failed to commit transaction: %v", err) 75 | } 76 | 77 | return nil 78 | } -------------------------------------------------------------------------------- /internal/repository/rental_equipment_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "go-barcode-webapp/internal/models" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type RentalEquipmentRepository struct { 11 | db *Database 12 | } 13 | 14 | func NewRentalEquipmentRepository(db *Database) *RentalEquipmentRepository { 15 | return &RentalEquipmentRepository{db: db} 16 | } 17 | 18 | // GetAllRentalEquipment returns all rental equipment items 19 | func (r *RentalEquipmentRepository) GetAllRentalEquipment(rentalEquipment *[]models.RentalEquipment) error { 20 | return r.db.Find(rentalEquipment).Error 21 | } 22 | 23 | // GetRentalEquipmentByID returns a specific rental equipment item by ID 24 | func (r *RentalEquipmentRepository) GetRentalEquipmentByID(equipmentID uint, rentalEquipment *models.RentalEquipment) error { 25 | return r.db.First(rentalEquipment, equipmentID).Error 26 | } 27 | 28 | // CreateRentalEquipment creates a new rental equipment item 29 | func (r *RentalEquipmentRepository) CreateRentalEquipment(rentalEquipment *models.RentalEquipment) error { 30 | return r.db.Create(rentalEquipment).Error 31 | } 32 | 33 | // UpdateRentalEquipment updates an existing rental equipment item 34 | func (r *RentalEquipmentRepository) UpdateRentalEquipment(rentalEquipment *models.RentalEquipment) error { 35 | return r.db.Save(rentalEquipment).Error 36 | } 37 | 38 | // DeleteRentalEquipment deletes a rental equipment item 39 | func (r *RentalEquipmentRepository) DeleteRentalEquipment(equipmentID uint) error { 40 | // First check if the equipment is used in any jobs 41 | var count int64 42 | err := r.db.Model(&models.JobRentalEquipment{}).Where("equipment_id = ?", equipmentID).Count(&count).Error 43 | if err != nil { 44 | return fmt.Errorf("failed to check equipment usage: %v", err) 45 | } 46 | 47 | if count > 0 { 48 | return fmt.Errorf("cannot delete equipment that is used in %d job(s)", count) 49 | } 50 | 51 | return r.db.Delete(&models.RentalEquipment{}, equipmentID).Error 52 | } 53 | 54 | // AddRentalToJob adds rental equipment to a job 55 | func (r *RentalEquipmentRepository) AddRentalToJob(jobRental *models.JobRentalEquipment) error { 56 | // Get rental equipment to calculate total cost 57 | var equipment models.RentalEquipment 58 | if err := r.db.First(&equipment, jobRental.EquipmentID).Error; err != nil { 59 | return fmt.Errorf("rental equipment not found: %v", err) 60 | } 61 | 62 | // Calculate total cost 63 | jobRental.TotalCost = equipment.RentalPrice * float64(jobRental.Quantity) * float64(jobRental.DaysUsed) 64 | 65 | // Check if already exists, then update or create 66 | var existingRental models.JobRentalEquipment 67 | err := r.db.Where("job_id = ? AND equipment_id = ?", jobRental.JobID, jobRental.EquipmentID).First(&existingRental).Error 68 | 69 | if err == gorm.ErrRecordNotFound { 70 | // Create new 71 | return r.db.Create(jobRental).Error 72 | } else if err != nil { 73 | return err 74 | } else { 75 | // Update existing 76 | existingRental.Quantity = jobRental.Quantity 77 | existingRental.DaysUsed = jobRental.DaysUsed 78 | existingRental.TotalCost = jobRental.TotalCost 79 | existingRental.Notes = jobRental.Notes 80 | return r.db.Save(&existingRental).Error 81 | } 82 | } 83 | 84 | // CreateRentalEquipmentFromManualEntry creates rental equipment and adds it to job in one transaction 85 | func (r *RentalEquipmentRepository) CreateRentalEquipmentFromManualEntry(request *models.ManualRentalEntryRequest, createdBy *uint) (*models.RentalEquipment, *models.JobRentalEquipment, error) { 86 | var rentalEquipment *models.RentalEquipment 87 | var jobRental *models.JobRentalEquipment 88 | 89 | err := r.db.Transaction(func(tx *gorm.DB) error { 90 | // Create rental equipment 91 | rentalEquipment = &models.RentalEquipment{ 92 | ProductName: request.ProductName, 93 | SupplierName: request.SupplierName, 94 | RentalPrice: request.RentalPrice, 95 | Category: request.Category, 96 | Description: request.Description, 97 | Notes: request.Notes, 98 | IsActive: true, 99 | CreatedBy: createdBy, 100 | } 101 | 102 | if err := tx.Create(rentalEquipment).Error; err != nil { 103 | return fmt.Errorf("failed to create rental equipment: %v", err) 104 | } 105 | 106 | // Add to job 107 | totalCost := request.RentalPrice * float64(request.Quantity) * float64(request.DaysUsed) 108 | 109 | jobRental = &models.JobRentalEquipment{ 110 | JobID: request.JobID, 111 | EquipmentID: rentalEquipment.EquipmentID, 112 | Quantity: request.Quantity, 113 | DaysUsed: request.DaysUsed, 114 | TotalCost: totalCost, 115 | Notes: request.Notes, 116 | } 117 | 118 | if err := tx.Create(jobRental).Error; err != nil { 119 | return fmt.Errorf("failed to add rental to job: %v", err) 120 | } 121 | 122 | return nil 123 | }) 124 | 125 | return rentalEquipment, jobRental, err 126 | } 127 | 128 | // GetJobRentalEquipment returns all rental equipment for a specific job 129 | func (r *RentalEquipmentRepository) GetJobRentalEquipment(jobID uint, jobRentals *[]models.JobRentalEquipment) error { 130 | return r.db.Preload("RentalEquipment").Where("job_id = ?", jobID).Find(jobRentals).Error 131 | } 132 | 133 | // RemoveRentalFromJob removes rental equipment from a job 134 | func (r *RentalEquipmentRepository) RemoveRentalFromJob(jobID, equipmentID uint) error { 135 | return r.db.Where("job_id = ? AND equipment_id = ?", jobID, equipmentID).Delete(&models.JobRentalEquipment{}).Error 136 | } 137 | 138 | // GetRentalEquipmentAnalytics returns analytics data for rental equipment 139 | func (r *RentalEquipmentRepository) GetRentalEquipmentAnalytics() (*models.RentalEquipmentAnalytics, error) { 140 | analytics := &models.RentalEquipmentAnalytics{} 141 | 142 | // Basic counts 143 | var totalCount, activeCount int64 144 | r.db.Model(&models.RentalEquipment{}).Count(&totalCount) 145 | r.db.Model(&models.RentalEquipment{}).Where("is_active = ?", true).Count(&activeCount) 146 | analytics.TotalEquipmentItems = int(totalCount) 147 | analytics.ActiveEquipmentItems = int(activeCount) 148 | 149 | // Count distinct suppliers 150 | var suppliers []string 151 | r.db.Model(&models.RentalEquipment{}).Distinct("supplier_name").Pluck("supplier_name", &suppliers) 152 | analytics.TotalSuppliersCount = len(suppliers) 153 | 154 | // Total rental revenue 155 | r.db.Model(&models.JobRentalEquipment{}).Select("COALESCE(SUM(total_cost), 0)").Scan(&analytics.TotalRentalRevenue) 156 | 157 | // Basic category breakdown (simplified for now) 158 | var categories []models.RentalCategoryBreakdown 159 | 160 | // Get categories with equipment count 161 | type CategorySummary struct { 162 | Category string 163 | EquipmentCount int64 164 | TotalRevenue float64 165 | } 166 | 167 | var categorySummaries []CategorySummary 168 | r.db.Model(&models.RentalEquipment{}). 169 | Select("COALESCE(category, 'Uncategorized') as category, COUNT(*) as equipment_count"). 170 | Group("category"). 171 | Find(&categorySummaries) 172 | 173 | for _, summary := range categorySummaries { 174 | var avgRevenue float64 175 | if summary.EquipmentCount > 0 { 176 | avgRevenue = summary.TotalRevenue / float64(summary.EquipmentCount) 177 | } 178 | 179 | categories = append(categories, models.RentalCategoryBreakdown{ 180 | Category: summary.Category, 181 | EquipmentCount: int(summary.EquipmentCount), 182 | TotalRevenue: summary.TotalRevenue, 183 | UsageCount: 0, // Simplified for now 184 | AvgRevenuePerEquipment: avgRevenue, 185 | }) 186 | } 187 | 188 | // For simplicity, using simplified data for now 189 | analytics.MostUsedEquipment = []models.MostUsedRentalEquipment{} 190 | analytics.TopSuppliers = []models.TopRentalSupplier{} 191 | analytics.CategoryBreakdown = categories 192 | analytics.MonthlyRentalRevenue = []models.MonthlyRentalRevenue{} 193 | 194 | return analytics, nil 195 | } -------------------------------------------------------------------------------- /internal/repository/status_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "go-barcode-webapp/internal/models" 4 | 5 | type StatusRepository struct { 6 | db *Database 7 | } 8 | 9 | func NewStatusRepository(db *Database) *StatusRepository { 10 | return &StatusRepository{db: db} 11 | } 12 | 13 | func (r *StatusRepository) List() ([]models.Status, error) { 14 | var statuses []models.Status 15 | err := r.db.Find(&statuses).Error 16 | return statuses, err 17 | } 18 | 19 | func (r *StatusRepository) GetByID(id uint) (*models.Status, error) { 20 | var status models.Status 21 | err := r.db.First(&status, id).Error 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &status, nil 26 | } -------------------------------------------------------------------------------- /internal/routes/scan_fallback.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go-barcode-webapp/internal/scan" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // ScanFallbackHandler handles server-side barcode decoding 12 | type ScanFallbackHandler struct { 13 | decoder *scan.ServerDecoder 14 | enabled bool // Feature flag to enable/disable server-side decoding 15 | } 16 | 17 | // NewScanFallbackHandler creates a new scan fallback handler 18 | func NewScanFallbackHandler() *ScanFallbackHandler { 19 | return &ScanFallbackHandler{ 20 | decoder: scan.NewServerDecoder(), 21 | enabled: false, // Disabled by default - enable via environment variable 22 | } 23 | } 24 | 25 | // SetEnabled enables or disables the fallback decoder 26 | func (h *ScanFallbackHandler) SetEnabled(enabled bool) { 27 | h.enabled = enabled 28 | } 29 | 30 | // IsEnabled returns whether the fallback decoder is enabled 31 | func (h *ScanFallbackHandler) IsEnabled() bool { 32 | return h.enabled 33 | } 34 | 35 | // DecodeFallback handles server-side decode requests 36 | func (h *ScanFallbackHandler) DecodeFallback(c *gin.Context) { 37 | // Check if feature is enabled 38 | if !h.enabled { 39 | c.JSON(http.StatusNotFound, gin.H{ 40 | "error": "FEATURE_DISABLED", 41 | "message": "Server-side decode is disabled", 42 | }) 43 | return 44 | } 45 | 46 | // Parse request 47 | var req scan.DecodeRequest 48 | if err := c.ShouldBindJSON(&req); err != nil { 49 | c.JSON(http.StatusBadRequest, gin.H{ 50 | "error": "INVALID_REQUEST", 51 | "message": err.Error(), 52 | }) 53 | return 54 | } 55 | 56 | // Validate request 57 | if req.Width <= 0 || req.Height <= 0 { 58 | c.JSON(http.StatusBadRequest, gin.H{ 59 | "error": "INVALID_DIMENSIONS", 60 | "message": "Width and height must be positive", 61 | }) 62 | return 63 | } 64 | 65 | if req.ImageData == "" { 66 | c.JSON(http.StatusBadRequest, gin.H{ 67 | "error": "MISSING_IMAGE_DATA", 68 | "message": "Image data is required", 69 | }) 70 | return 71 | } 72 | 73 | // Process decode request 74 | response := h.decoder.Decode(&req) 75 | 76 | // Return appropriate HTTP status 77 | if response.Success { 78 | c.JSON(http.StatusOK, response) 79 | } else { 80 | // Return 422 for decode failures (valid request, but no barcode found) 81 | c.JSON(http.StatusUnprocessableEntity, response) 82 | } 83 | } 84 | 85 | // GetDecoderStatus returns the status of the fallback decoder 86 | func (h *ScanFallbackHandler) GetDecoderStatus(c *gin.Context) { 87 | c.JSON(http.StatusOK, gin.H{ 88 | "enabled": h.enabled, 89 | "status": "ready", 90 | "serverSide": true, 91 | "supportedFormats": []string{ 92 | "CODE_128", "CODE_39", "EAN_13", "EAN_8", 93 | "UPC_A", "UPC_E", "ITF", "QR_CODE", 94 | }, 95 | }) 96 | } 97 | 98 | // SetupScanFallbackRoutes sets up the fallback decode routes 99 | func SetupScanFallbackRoutes(r *gin.Engine, handler *ScanFallbackHandler) { 100 | // API group for scan fallback 101 | api := r.Group("/api/scan") 102 | { 103 | // Decode endpoint 104 | api.POST("/decode", handler.DecodeFallback) 105 | 106 | // Status endpoint 107 | api.GET("/status", handler.GetDecoderStatus) 108 | } 109 | } 110 | 111 | // ScanFallbackMiddleware adds rate limiting for scan fallback requests 112 | func ScanFallbackMiddleware() gin.HandlerFunc { 113 | // Simple rate limiting - in production you might want to use redis 114 | requestCounts := make(map[string]int) 115 | 116 | return gin.HandlerFunc(func(c *gin.Context) { 117 | // Rate limit by IP 118 | clientIP := c.ClientIP() 119 | 120 | // Simple rate limiting: max 60 requests per minute per IP 121 | if count, exists := requestCounts[clientIP]; exists && count > 60 { 122 | c.JSON(http.StatusTooManyRequests, gin.H{ 123 | "error": "RATE_LIMITED", 124 | "message": "Too many requests. Server-side decode is rate limited.", 125 | }) 126 | c.Abort() 127 | return 128 | } 129 | 130 | requestCounts[clientIP]++ 131 | 132 | // Reset counter periodically (simple implementation) 133 | // In production, use a proper rate limiter like golang.org/x/time/rate 134 | 135 | c.Next() 136 | }) 137 | } -------------------------------------------------------------------------------- /internal/services/barcode_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "image/png" 7 | 8 | "github.com/boombuler/barcode" 9 | "github.com/boombuler/barcode/code128" 10 | "github.com/skip2/go-qrcode" 11 | ) 12 | 13 | type BarcodeService struct{} 14 | 15 | func NewBarcodeService() *BarcodeService { 16 | return &BarcodeService{} 17 | } 18 | 19 | func (s *BarcodeService) GenerateQRCode(data string, size int) ([]byte, error) { 20 | pngBytes, err := qrcode.Encode(data, qrcode.Medium, size) 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to create QR code: %w", err) 23 | } 24 | 25 | return pngBytes, nil 26 | } 27 | 28 | func (s *BarcodeService) GenerateBarcode(data string) ([]byte, error) { 29 | // Create Code128 barcode 30 | bc, err := code128.Encode(data) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to encode barcode: %w", err) 33 | } 34 | 35 | // Scale the barcode to reasonable size 36 | scaledBC, err := barcode.Scale(bc, 200, 100) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to scale barcode: %w", err) 39 | } 40 | 41 | // Convert to PNG bytes 42 | var buf bytes.Buffer 43 | err = png.Encode(&buf, scaledBC) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to encode barcode as PNG: %w", err) 46 | } 47 | 48 | return buf.Bytes(), nil 49 | } 50 | 51 | func (s *BarcodeService) GenerateDeviceQR(deviceID string) ([]byte, error) { 52 | data := fmt.Sprintf("DEVICE:%s", deviceID) 53 | return s.GenerateQRCode(data, 256) 54 | } 55 | 56 | func (s *BarcodeService) GenerateDeviceBarcode(deviceID string) ([]byte, error) { 57 | return s.GenerateBarcode(deviceID) 58 | } -------------------------------------------------------------------------------- /jobscanner.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=JobScanner Pro - Equipment Management System 3 | After=network.target mysql.service 4 | Wants=network-online.target 5 | Requires=mysql.service 6 | 7 | [Service] 8 | Type=exec 9 | User=www-data 10 | Group=www-data 11 | WorkingDirectory=/opt/go-barcode-webapp 12 | ExecStart=/opt/go-barcode-webapp/server -config=config.production.direct.json 13 | ExecReload=/bin/kill -HUP $MAINPID 14 | Restart=always 15 | RestartSec=5 16 | TimeoutStopSec=20 17 | KillMode=mixed 18 | 19 | # Security settings 20 | NoNewPrivileges=true 21 | PrivateTmp=true 22 | ProtectSystem=strict 23 | ProtectHome=true 24 | ReadWritePaths=/opt/go-barcode-webapp/logs 25 | 26 | # Environment 27 | Environment=GIN_MODE=release 28 | Environment=TZ=Europe/Berlin 29 | 30 | # Logging 31 | StandardOutput=append:/opt/go-barcode-webapp/logs/production.log 32 | StandardError=append:/opt/go-barcode-webapp/logs/production.log 33 | 34 | [Install] 35 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /migrations/001_initial_schema.sql: -------------------------------------------------------------------------------- 1 | -- JobScanner Pro - Initial Database Schema 2 | 3 | CREATE DATABASE IF NOT EXISTS jobscanner CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 4 | USE jobscanner; 5 | 6 | -- Customers table 7 | CREATE TABLE customers ( 8 | id INT AUTO_INCREMENT PRIMARY KEY, 9 | name VARCHAR(255) NOT NULL, 10 | email VARCHAR(255), 11 | phone VARCHAR(50), 12 | address TEXT, 13 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 14 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 15 | INDEX idx_name (name), 16 | INDEX idx_email (email) 17 | ); 18 | 19 | -- Status table 20 | CREATE TABLE statuses ( 21 | id INT AUTO_INCREMENT PRIMARY KEY, 22 | name VARCHAR(100) NOT NULL UNIQUE, 23 | description TEXT, 24 | color VARCHAR(7) DEFAULT '#007bff', 25 | INDEX idx_name (name) 26 | ); 27 | 28 | -- Jobs table 29 | CREATE TABLE jobs ( 30 | id INT AUTO_INCREMENT PRIMARY KEY, 31 | customer_id INT NOT NULL, 32 | status_id INT NOT NULL, 33 | title VARCHAR(255) NOT NULL, 34 | description TEXT, 35 | start_date DATE, 36 | end_date DATE, 37 | revenue DECIMAL(10,2) DEFAULT 0.00, 38 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 39 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 40 | deleted_at TIMESTAMP NULL, 41 | FOREIGN KEY (customer_id) REFERENCES customers(id), 42 | FOREIGN KEY (status_id) REFERENCES statuses(id), 43 | INDEX idx_customer (customer_id), 44 | INDEX idx_status (status_id), 45 | INDEX idx_dates (start_date, end_date), 46 | INDEX idx_revenue (revenue), 47 | INDEX idx_title (title), 48 | INDEX idx_deleted_at (deleted_at) 49 | ); 50 | 51 | -- Devices table 52 | CREATE TABLE devices ( 53 | id INT AUTO_INCREMENT PRIMARY KEY, 54 | serial_no VARCHAR(255) UNIQUE NOT NULL, 55 | name VARCHAR(255) NOT NULL, 56 | description TEXT, 57 | category VARCHAR(100), 58 | price DECIMAL(10,2) DEFAULT 0.00, 59 | available BOOLEAN DEFAULT TRUE, 60 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 61 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 62 | INDEX idx_serial (serial_no), 63 | INDEX idx_name (name), 64 | INDEX idx_category (category), 65 | INDEX idx_available (available) 66 | ); 67 | 68 | -- Products table 69 | CREATE TABLE products ( 70 | id INT AUTO_INCREMENT PRIMARY KEY, 71 | name VARCHAR(255) NOT NULL, 72 | description TEXT, 73 | category VARCHAR(100), 74 | price DECIMAL(10,2) DEFAULT 0.00, 75 | active BOOLEAN DEFAULT TRUE, 76 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 77 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 78 | INDEX idx_name (name), 79 | INDEX idx_category (category), 80 | INDEX idx_active (active) 81 | ); 82 | 83 | -- Job-Device relationship table 84 | CREATE TABLE job_devices ( 85 | id INT AUTO_INCREMENT PRIMARY KEY, 86 | job_id INT NOT NULL, 87 | device_id INT NOT NULL, 88 | price DECIMAL(10,2) DEFAULT 0.00, 89 | assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 90 | removed_at TIMESTAMP NULL, 91 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 92 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 93 | FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE, 94 | FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE, 95 | INDEX idx_job (job_id), 96 | INDEX idx_device (device_id), 97 | INDEX idx_assigned (assigned_at), 98 | INDEX idx_removed (removed_at), 99 | UNIQUE KEY unique_active_assignment (job_id, device_id, removed_at) 100 | ); 101 | 102 | -- Insert default statuses 103 | INSERT INTO statuses (name, description, color) VALUES 104 | ('Planning', 'Job is in planning phase', '#6c757d'), 105 | ('Active', 'Job is currently active', '#28a745'), 106 | ('Completed', 'Job has been completed', '#007bff'), 107 | ('Cancelled', 'Job has been cancelled', '#dc3545'), 108 | ('On Hold', 'Job is temporarily on hold', '#ffc107'); 109 | 110 | -- Sample data removed for production use 111 | -- To add initial data, use the application interface or create separate data import scripts 112 | 113 | -- Create views for commonly used queries 114 | CREATE VIEW job_summary AS 115 | SELECT 116 | j.id, 117 | j.title, 118 | j.description, 119 | j.start_date, 120 | j.end_date, 121 | j.revenue, 122 | j.created_at, 123 | j.updated_at, 124 | c.name as customer_name, 125 | s.name as status_name, 126 | s.color as status_color, 127 | COUNT(DISTINCT jd.device_id) as device_count, 128 | COALESCE(SUM(jd.price), 0) as total_device_revenue 129 | FROM jobs j 130 | LEFT JOIN customers c ON j.customer_id = c.id 131 | LEFT JOIN statuses s ON j.status_id = s.id 132 | LEFT JOIN job_devices jd ON j.id = jd.job_id AND jd.removed_at IS NULL 133 | WHERE j.deleted_at IS NULL 134 | GROUP BY j.id, j.title, j.description, j.start_date, j.end_date, j.revenue, j.created_at, j.updated_at, c.name, s.name, s.color; 135 | 136 | CREATE VIEW device_status AS 137 | SELECT 138 | d.id, 139 | d.serial_no, 140 | d.name, 141 | d.description, 142 | d.category, 143 | d.price, 144 | d.available, 145 | d.created_at, 146 | d.updated_at, 147 | CASE 148 | WHEN jd.job_id IS NOT NULL THEN FALSE 149 | ELSE TRUE 150 | END as is_free, 151 | jd.job_id as current_job_id, 152 | j.title as current_job_title 153 | FROM devices d 154 | LEFT JOIN job_devices jd ON d.id = jd.device_id AND jd.removed_at IS NULL 155 | LEFT JOIN jobs j ON jd.job_id = j.id AND j.deleted_at IS NULL; -------------------------------------------------------------------------------- /migrations/006_performance_indexes.sql: -------------------------------------------------------------------------------- 1 | -- Performance optimization indexes 2 | -- Add indexes for commonly searched fields and join operations 3 | 4 | -- Jobs table indexes 5 | CREATE INDEX IF NOT EXISTS idx_jobs_description ON jobs(description); 6 | CREATE INDEX IF NOT EXISTS idx_jobs_end_date ON jobs(endDate); 7 | CREATE INDEX IF NOT EXISTS idx_jobs_customer_id ON jobs(customerID); 8 | CREATE INDEX IF NOT EXISTS idx_jobs_status_id ON jobs(statusID); 9 | 10 | -- Customers table indexes 11 | CREATE INDEX IF NOT EXISTS idx_customers_search ON customers(companyname, firstname, lastname); 12 | CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email); 13 | 14 | -- Devices table indexes 15 | CREATE INDEX IF NOT EXISTS idx_devices_search ON devices(deviceID, serialnumber); 16 | CREATE INDEX IF NOT EXISTS idx_devices_product_id ON devices(productID); 17 | CREATE INDEX IF NOT EXISTS idx_devices_status ON devices(status); 18 | 19 | -- Job devices junction table 20 | CREATE INDEX IF NOT EXISTS idx_job_devices_job_id ON jobdevices(jobID); 21 | CREATE INDEX IF NOT EXISTS idx_job_devices_device_id ON jobdevices(deviceID); 22 | CREATE INDEX IF NOT EXISTS idx_job_devices_composite ON jobdevices(jobID, deviceID); 23 | 24 | -- Financial transactions indexes 25 | CREATE INDEX IF NOT EXISTS idx_financial_transactions_status_type ON financial_transactions(status, type); 26 | CREATE INDEX IF NOT EXISTS idx_financial_transactions_date ON financial_transactions(transaction_date); 27 | CREATE INDEX IF NOT EXISTS idx_financial_transactions_due_date ON financial_transactions(due_date); 28 | CREATE INDEX IF NOT EXISTS idx_financial_transactions_customer ON financial_transactions(customerID); 29 | 30 | -- Invoices table indexes 31 | CREATE INDEX IF NOT EXISTS idx_invoices_customer_id ON invoices(customerID); 32 | CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status); 33 | CREATE INDEX IF NOT EXISTS idx_invoices_date ON invoices(invoice_date); 34 | 35 | -- Sessions table cleanup index 36 | CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); 37 | 38 | -- Composite indexes for common queries 39 | CREATE INDEX IF NOT EXISTS idx_jobs_customer_status ON jobs(customerID, statusID); 40 | CREATE INDEX IF NOT EXISTS idx_devices_product_status ON devices(productID, status); -------------------------------------------------------------------------------- /migrations/007_equipment_packages.sql: -------------------------------------------------------------------------------- 1 | -- Enhanced Equipment Packages Migration (MySQL Compatible) 2 | -- Add new fields for production-ready equipment packages 3 | 4 | -- Add new columns to equipment_packages table 5 | -- Note: Run each ALTER TABLE separately if you get syntax errors 6 | 7 | -- Add max_rental_days column 8 | ALTER TABLE equipment_packages ADD COLUMN max_rental_days INT NULL; 9 | 10 | -- Add category column 11 | ALTER TABLE equipment_packages ADD COLUMN category VARCHAR(50) NULL; 12 | 13 | -- Add tags column 14 | ALTER TABLE equipment_packages ADD COLUMN tags TEXT NULL; 15 | 16 | -- Add last_used_at column 17 | ALTER TABLE equipment_packages ADD COLUMN last_used_at TIMESTAMP NULL; 18 | 19 | -- Add total_revenue column 20 | ALTER TABLE equipment_packages ADD COLUMN total_revenue DECIMAL(12,2) DEFAULT 0.00; 21 | 22 | -- Add indexes for better performance 23 | CREATE INDEX idx_equipment_packages_category ON equipment_packages(category); 24 | CREATE INDEX idx_equipment_packages_active ON equipment_packages(is_active); 25 | CREATE INDEX idx_equipment_packages_usage ON equipment_packages(usage_count); 26 | 27 | -- Add indexes for package_devices table 28 | CREATE INDEX idx_package_devices_package_id ON package_devices(package_id); 29 | CREATE INDEX idx_package_devices_device_id ON package_devices(device_id); 30 | 31 | -- Update existing package_items to be valid JSON if NULL 32 | UPDATE equipment_packages 33 | SET package_items = '[]' 34 | WHERE package_items IS NULL OR package_items = ''; 35 | 36 | -- Sample equipment packages removed for production use 37 | -- Create packages through the application interface as needed -------------------------------------------------------------------------------- /migrations/008_company_settings.sql: -------------------------------------------------------------------------------- 1 | -- Migration: Company Settings Table 2 | -- Description: Creates company_settings table for storing company information 3 | -- Required for German invoice compliance (GoBD) 4 | 5 | CREATE TABLE IF NOT EXISTS `company_settings` ( 6 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 7 | `company_name` VARCHAR(255) NOT NULL, 8 | `address_line1` VARCHAR(255) NULL, 9 | `address_line2` VARCHAR(255) NULL, 10 | `city` VARCHAR(100) NULL, 11 | `state` VARCHAR(100) NULL, 12 | `postal_code` VARCHAR(20) NULL, 13 | `country` VARCHAR(100) NULL DEFAULT 'Deutschland', 14 | `phone` VARCHAR(50) NULL, 15 | `email` VARCHAR(255) NULL, 16 | `website` VARCHAR(255) NULL, 17 | `tax_number` VARCHAR(50) NULL COMMENT 'Steuernummer für deutsche Unternehmen', 18 | `vat_number` VARCHAR(50) NULL COMMENT 'USt-IdNr für EU-Unternehmen', 19 | `logo_path` VARCHAR(500) NULL, 20 | `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 22 | PRIMARY KEY (`id`) 23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Company settings for invoice generation and German compliance'; 24 | 25 | -- Insert default company settings if none exist 26 | INSERT IGNORE INTO `company_settings` ( 27 | `id`, 28 | `company_name`, 29 | `address_line1`, 30 | `city`, 31 | `postal_code`, 32 | `country`, 33 | `phone`, 34 | `email`, 35 | `tax_number`, 36 | `vat_number` 37 | ) VALUES ( 38 | 1, 39 | 'TS RentalCore GmbH', 40 | NULL, 41 | NULL, 42 | NULL, 43 | NULL, 44 | NULL, 45 | NULL, 46 | NULL, 47 | NULL 48 | ); 49 | 50 | -- Add index for faster lookups (should only be one record anyway) 51 | CREATE INDEX IF NOT EXISTS `idx_company_settings_updated` ON `company_settings` (`updated_at`); 52 | 53 | -- Ensure only one company settings record exists (business rule) 54 | -- This will be enforced in the application layer -------------------------------------------------------------------------------- /migrations/009_company_settings_german_fields.sql: -------------------------------------------------------------------------------- 1 | -- Migration: Add German Business Fields to Company Settings 2 | -- Description: Adds banking, legal, and invoice text fields for German compliance (GoBD) 3 | -- Date: 2025-06-20 4 | 5 | -- Add German banking fields 6 | ALTER TABLE `company_settings` 7 | ADD COLUMN `bank_name` VARCHAR(255) NULL COMMENT 'Name der Bank für Rechnungen', 8 | ADD COLUMN `iban` VARCHAR(34) NULL COMMENT 'IBAN für Zahlungen', 9 | ADD COLUMN `bic` VARCHAR(11) NULL COMMENT 'BIC/SWIFT Code', 10 | ADD COLUMN `account_holder` VARCHAR(255) NULL COMMENT 'Kontoinhaber falls abweichend'; 11 | 12 | -- Add German legal fields 13 | ALTER TABLE `company_settings` 14 | ADD COLUMN `ceo_name` VARCHAR(255) NULL COMMENT 'Name des Geschäftsführers', 15 | ADD COLUMN `register_court` VARCHAR(255) NULL COMMENT 'Registergericht', 16 | ADD COLUMN `register_number` VARCHAR(100) NULL COMMENT 'Handelsregisternummer'; 17 | 18 | -- Add invoice text fields 19 | ALTER TABLE `company_settings` 20 | ADD COLUMN `footer_text` TEXT NULL COMMENT 'Footer-Text für Rechnungen', 21 | ADD COLUMN `payment_terms_text` TEXT NULL COMMENT 'Zahlungsbedingungen Text'; 22 | 23 | -- Update existing record with German defaults if it exists 24 | UPDATE `company_settings` 25 | SET 26 | `country` = 'Deutschland', 27 | `footer_text` = 'Vielen Dank für Ihr Vertrauen!\n\nBei Fragen zu dieser Rechnung stehen wir Ihnen gerne zur Verfügung.', 28 | `payment_terms_text` = 'Zahlbar innerhalb von 30 Tagen ohne Abzug.\n\nBei Zahlungsverzug behalten wir uns vor, Verzugszinsen in Höhe von 9 Prozentpunkten über dem Basiszinssatz zu berechnen.' 29 | WHERE `id` = 1; 30 | 31 | -- Add indexes for better performance 32 | CREATE INDEX IF NOT EXISTS `idx_company_settings_iban` ON `company_settings` (`iban`); 33 | CREATE INDEX IF NOT EXISTS `idx_company_settings_register_number` ON `company_settings` (`register_number`); 34 | 35 | -- Show completion status 36 | SELECT 'German business fields migration completed successfully!' as status; -------------------------------------------------------------------------------- /migrations/010_fix_sessions_foreign_key.sql: -------------------------------------------------------------------------------- 1 | -- Drop the procedure if it exists to ensure a clean run 2 | DROP PROCEDURE IF EXISTS fix_sessions_and_users; 3 | 4 | DELIMITER $$ 5 | 6 | -- Create a procedure to encapsulate the migration logic 7 | CREATE PROCEDURE fix_sessions_and_users() 8 | BEGIN 9 | -- Check if a primary key already exists on the sessions table. 10 | SET @pk_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'sessions' AND CONSTRAINT_TYPE = 'PRIMARY KEY'); 11 | 12 | -- Conditionally add the primary key to the sessions table on session_id. 13 | SET @sql_pk = IF(@pk_exists = 0, 'ALTER TABLE `sessions` ADD PRIMARY KEY (`session_id`)', 'SELECT "Primary key already exists on sessions table, skipping."'); 14 | PREPARE stmt_pk FROM @sql_pk; 15 | EXECUTE stmt_pk; 16 | DEALLOCATE PREPARE stmt_pk; 17 | 18 | -- The user_id in the sessions table is not unique, so a foreign key from users to sessions is not possible. 19 | -- This appears to be a logic error in a previous migration. 20 | -- We will attempt to drop this constraint if it exists. 21 | SET @fk_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND CONSTRAINT_NAME = 'fk_sessions_user'); 22 | 23 | -- Conditionally drop the foreign key 24 | SET @sql_fk = IF(@fk_exists > 0, 'ALTER TABLE `users` DROP FOREIGN KEY `fk_sessions_user`', 'SELECT "Foreign key fk_sessions_user does not exist, skipping."'); 25 | PREPARE stmt_fk FROM @sql_fk; 26 | EXECUTE stmt_fk; 27 | DEALLOCATE PREPARE stmt_fk; 28 | END$$ 29 | 30 | DELIMITER ; 31 | 32 | -- Execute the stored procedure to apply the fixes 33 | CALL fix_sessions_and_users(); 34 | 35 | -- Clean up by dropping the procedure 36 | DROP PROCEDURE fix_sessions_and_users; 37 | -------------------------------------------------------------------------------- /migrations/011_add_email_settings.sql: -------------------------------------------------------------------------------- 1 | -- Add email configuration fields to company_settings table 2 | ALTER TABLE company_settings 3 | ADD COLUMN smtp_host VARCHAR(255), 4 | ADD COLUMN smtp_port INT, 5 | ADD COLUMN smtp_username VARCHAR(255), 6 | ADD COLUMN smtp_password VARCHAR(255), 7 | ADD COLUMN smtp_from_email VARCHAR(255), 8 | ADD COLUMN smtp_from_name VARCHAR(255), 9 | ADD COLUMN smtp_use_tls BOOLEAN DEFAULT TRUE; -------------------------------------------------------------------------------- /migrations/012_fix_user_preferences_fk.sql: -------------------------------------------------------------------------------- 1 | -- Fix Foreign Key Constraint for User Preferences 2 | -- The constraint was backwards - users should not reference user_preferences 3 | -- Instead, user_preferences should reference users 4 | 5 | -- First, drop the incorrect foreign key constraint 6 | ALTER TABLE `users` DROP FOREIGN KEY `fk_user_preferences_user`; 7 | 8 | -- Then, add the correct foreign key constraint on the user_preferences table 9 | -- This ensures user_preferences.user_id references users.userID (correct direction) 10 | ALTER TABLE `user_preferences` 11 | ADD CONSTRAINT `fk_user_preferences_user` 12 | FOREIGN KEY (`user_id`) REFERENCES `users` (`userID`) 13 | ON DELETE CASCADE ON UPDATE CASCADE; -------------------------------------------------------------------------------- /migrations/013_add_2fa_and_passkey_tables.sql: -------------------------------------------------------------------------------- 1 | -- ================================================================ 2 | -- MIGRATION 013: 2FA AND PASSKEY AUTHENTICATION TABLES 3 | -- Creates the missing tables for two-factor authentication and passkeys 4 | -- ================================================================ 5 | 6 | -- Start transaction for atomic migration 7 | START TRANSACTION; 8 | 9 | -- ================================================================ 10 | -- 1. TWO-FACTOR AUTHENTICATION TABLE 11 | -- ================================================================ 12 | 13 | CREATE TABLE user_2fa ( 14 | two_fa_id INT AUTO_INCREMENT PRIMARY KEY, 15 | user_id BIGINT UNSIGNED NOT NULL, 16 | secret VARCHAR(100) NOT NULL, 17 | qr_code_url TEXT, 18 | backup_codes JSON, 19 | is_enabled BOOLEAN DEFAULT FALSE, 20 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 21 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 22 | last_used_at TIMESTAMP NULL, 23 | FOREIGN KEY (user_id) REFERENCES users(userID) ON DELETE CASCADE, 24 | UNIQUE KEY unique_user_2fa (user_id), 25 | INDEX idx_user_enabled (user_id, is_enabled) 26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 27 | 28 | -- ================================================================ 29 | -- 2. USER PASSKEYS TABLE 30 | -- ================================================================ 31 | 32 | CREATE TABLE user_passkeys ( 33 | passkey_id INT AUTO_INCREMENT PRIMARY KEY, 34 | user_id BIGINT UNSIGNED NOT NULL, 35 | name VARCHAR(100) NOT NULL, 36 | credential_id VARCHAR(255) NOT NULL UNIQUE, 37 | public_key TEXT NOT NULL, 38 | sign_count INT DEFAULT 0, 39 | aaguid VARCHAR(36), 40 | transport_methods JSON, 41 | is_active BOOLEAN DEFAULT TRUE, 42 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 43 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 44 | last_used_at TIMESTAMP NULL, 45 | FOREIGN KEY (user_id) REFERENCES users(userID) ON DELETE CASCADE, 46 | INDEX idx_user_active (user_id, is_active), 47 | INDEX idx_credential (credential_id), 48 | INDEX idx_last_used (last_used_at) 49 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 50 | 51 | -- ================================================================ 52 | -- 3. AUTHENTICATION ATTEMPTS TABLE 53 | -- ================================================================ 54 | 55 | CREATE TABLE authentication_attempts ( 56 | attempt_id BIGINT AUTO_INCREMENT PRIMARY KEY, 57 | user_id BIGINT UNSIGNED, 58 | method ENUM('password', '2fa', 'passkey', 'backup_code') NOT NULL, 59 | success BOOLEAN NOT NULL DEFAULT FALSE, 60 | ip_address VARCHAR(45), 61 | user_agent TEXT, 62 | attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 63 | failure_reason VARCHAR(255), 64 | session_id VARCHAR(191), 65 | FOREIGN KEY (user_id) REFERENCES users(userID) ON DELETE SET NULL, 66 | INDEX idx_user_time (user_id, attempted_at), 67 | INDEX idx_method_success (method, success), 68 | INDEX idx_ip_time (ip_address, attempted_at), 69 | INDEX idx_attempted_at (attempted_at) 70 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 71 | 72 | -- ================================================================ 73 | -- 4. USER PREFERENCES TABLE (Enhanced) 74 | -- ================================================================ 75 | 76 | CREATE TABLE user_preferences ( 77 | preference_id INT AUTO_INCREMENT PRIMARY KEY, 78 | user_id BIGINT UNSIGNED NOT NULL, 79 | language VARCHAR(5) DEFAULT 'de', 80 | theme VARCHAR(10) DEFAULT 'dark', 81 | time_zone VARCHAR(50) DEFAULT 'Europe/Berlin', 82 | date_format VARCHAR(20) DEFAULT 'DD.MM.YYYY', 83 | time_format VARCHAR(5) DEFAULT '24h', 84 | email_notifications BOOLEAN DEFAULT TRUE, 85 | system_notifications BOOLEAN DEFAULT TRUE, 86 | job_status_notifications BOOLEAN DEFAULT TRUE, 87 | device_alert_notifications BOOLEAN DEFAULT TRUE, 88 | items_per_page INT DEFAULT 25, 89 | default_view VARCHAR(20) DEFAULT 'list', 90 | show_advanced_options BOOLEAN DEFAULT FALSE, 91 | auto_save_enabled BOOLEAN DEFAULT TRUE, 92 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 93 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 94 | FOREIGN KEY (user_id) REFERENCES users(userID) ON DELETE CASCADE, 95 | UNIQUE KEY unique_user_preferences (user_id), 96 | INDEX idx_user_prefs (user_id) 97 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 98 | 99 | -- ================================================================ 100 | -- 5. WEBAUTHN SESSION TABLE 101 | -- ================================================================ 102 | 103 | CREATE TABLE webauthn_sessions ( 104 | session_id VARCHAR(191) PRIMARY KEY, 105 | user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, 106 | challenge VARCHAR(255) NOT NULL, 107 | session_type VARCHAR(50) NOT NULL, 108 | session_data TEXT, 109 | expires_at TIMESTAMP NOT NULL, 110 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 111 | INDEX idx_user_session (user_id, session_type), 112 | INDEX idx_expires (expires_at), 113 | INDEX idx_session_type (session_type) 114 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 115 | 116 | -- ================================================================ 117 | -- 6. SESSION MANAGEMENT TABLE (Enhanced) 118 | -- ================================================================ 119 | 120 | CREATE TABLE user_sessions ( 121 | session_id VARCHAR(191) PRIMARY KEY, 122 | user_id BIGINT UNSIGNED NOT NULL, 123 | ip_address VARCHAR(45), 124 | user_agent TEXT, 125 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 126 | last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 127 | expires_at TIMESTAMP NOT NULL, 128 | is_active BOOLEAN DEFAULT TRUE, 129 | device_info JSON, 130 | FOREIGN KEY (user_id) REFERENCES users(userID) ON DELETE CASCADE, 131 | INDEX idx_user_active (user_id, is_active), 132 | INDEX idx_expires (expires_at), 133 | INDEX idx_last_active (last_active) 134 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 135 | 136 | -- Commit the migration 137 | COMMIT; 138 | 139 | -- ================================================================ 140 | -- MIGRATION COMPLETE 141 | -- ================================================================ 142 | -- This migration adds: 143 | -- ✅ Complete 2FA support with backup codes 144 | -- ✅ WebAuthn passkey authentication 145 | -- ✅ Authentication attempt logging 146 | -- ✅ Enhanced user preferences 147 | -- ✅ Session management 148 | -- ================================================================ -------------------------------------------------------------------------------- /migrations/014_add_missing_webauthn_table.sql: -------------------------------------------------------------------------------- 1 | -- ================================================================ 2 | -- MIGRATION 014: ADD MISSING WEBAUTHN SESSION TABLE 3 | -- Creates webauthn_sessions table if it doesn't exist properly 4 | -- ================================================================ 5 | 6 | -- Drop and recreate the table to ensure proper structure 7 | DROP TABLE IF EXISTS webauthn_sessions; 8 | 9 | CREATE TABLE webauthn_sessions ( 10 | session_id VARCHAR(191) PRIMARY KEY, 11 | user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, 12 | challenge VARCHAR(255) NOT NULL, 13 | session_type VARCHAR(50) NOT NULL, 14 | session_data TEXT, 15 | expires_at TIMESTAMP NOT NULL, 16 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 17 | INDEX idx_user_session (user_id, session_type), 18 | INDEX idx_expires (expires_at), 19 | INDEX idx_session_type (session_type) 20 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -------------------------------------------------------------------------------- /migrations/021_create_rental_equipment_tables_corrected.down.sql: -------------------------------------------------------------------------------- 1 | -- Rollback migration for rental equipment system 2 | 3 | DROP TABLE IF EXISTS job_rental_equipment; 4 | DROP TABLE IF EXISTS rental_equipment; -------------------------------------------------------------------------------- /migrations/021_create_rental_equipment_tables_corrected.up.sql: -------------------------------------------------------------------------------- 1 | -- Migration: Create corrected tables for rental equipment system 2 | -- This creates tables that match the Go models exactly 3 | 4 | -- Table for rental equipment items (master list of available rental items) 5 | CREATE TABLE IF NOT EXISTS rental_equipment ( 6 | equipment_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 7 | product_name VARCHAR(200) NOT NULL, 8 | supplier_name VARCHAR(100) NOT NULL, 9 | rental_price DECIMAL(12, 2) NOT NULL DEFAULT 0.00, 10 | category VARCHAR(50), 11 | description VARCHAR(1000), 12 | notes VARCHAR(500), 13 | is_active BOOLEAN DEFAULT TRUE, 14 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 15 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 16 | created_by INT UNSIGNED, 17 | 18 | INDEX idx_product_name (product_name), 19 | INDEX idx_supplier_name (supplier_name), 20 | INDEX idx_category (category), 21 | INDEX idx_is_active (is_active) 22 | ); 23 | 24 | -- Bridge table for job-rental equipment assignments 25 | CREATE TABLE IF NOT EXISTS job_rental_equipment ( 26 | job_id INT NOT NULL, 27 | equipment_id INT UNSIGNED NOT NULL, 28 | quantity INT UNSIGNED NOT NULL DEFAULT 1, 29 | days_used INT UNSIGNED NOT NULL DEFAULT 1, 30 | total_cost DECIMAL(12, 2) NOT NULL, 31 | notes VARCHAR(500), 32 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 33 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 34 | 35 | PRIMARY KEY (job_id, equipment_id), 36 | FOREIGN KEY (job_id) REFERENCES jobs(jobID) ON DELETE CASCADE, 37 | FOREIGN KEY (equipment_id) REFERENCES rental_equipment(equipment_id) ON DELETE CASCADE, 38 | 39 | INDEX idx_job_id (job_id), 40 | INDEX idx_equipment_id (equipment_id), 41 | INDEX idx_created_at (created_at) 42 | ); 43 | 44 | -- Insert some example rental equipment items 45 | INSERT INTO rental_equipment (product_name, supplier_name, rental_price, description, category) VALUES 46 | ('LED Moving Head - Martin MAC Aura', 'Pro Rental GmbH', 45.00, 'Professional LED Moving Head Light', 'Lighting'), 47 | ('d&b V12 Line Array', 'Sound Solutions AG', 120.00, 'High-end Line Array Speaker System', 'Audio'), 48 | ('Truss System 3m Segment', 'Stage Tech Berlin', 15.00, '3 Meter Aluminum Truss Segment', 'Stage Equipment'), 49 | ('Haze Machine - Unique 2.1', 'Effect Masters', 35.00, 'Professional Haze Machine with DMX', 'Other'), 50 | ('LED Par 64 RGBW', 'Light Rental Pro', 12.00, 'RGBW LED Par with DMX Control', 'Lighting'), 51 | ('Wireless Microphone Shure ULXD2', 'Audio Rent Hamburg', 25.00, 'Professional Wireless Handheld Microphone', 'Audio'); -------------------------------------------------------------------------------- /migrations/022_fix_company_settings_datetime.sql: -------------------------------------------------------------------------------- 1 | -- Fix corrupted datetime values in company_settings table 2 | UPDATE company_settings 3 | SET created_at = CURRENT_TIMESTAMP, 4 | updated_at = CURRENT_TIMESTAMP 5 | WHERE created_at = '0000-00-00 00:00:00' 6 | OR updated_at = '0000-00-00 00:00:00' 7 | OR created_at IS NULL 8 | OR updated_at IS NULL; -------------------------------------------------------------------------------- /migrations/023_create_job_attachments_table.down.sql: -------------------------------------------------------------------------------- 1 | -- Drop job_attachments table 2 | DROP TABLE IF EXISTS job_attachments; -------------------------------------------------------------------------------- /migrations/023_create_job_attachments_table.up.sql: -------------------------------------------------------------------------------- 1 | -- Create job_attachments table for file attachments to jobs 2 | CREATE TABLE job_attachments ( 3 | attachment_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 4 | job_id INT NOT NULL, 5 | filename VARCHAR(255) NOT NULL, 6 | original_filename VARCHAR(255) NOT NULL, 7 | file_path VARCHAR(500) NOT NULL, 8 | file_size BIGINT NOT NULL, 9 | mime_type VARCHAR(100) NOT NULL, 10 | uploaded_by BIGINT UNSIGNED DEFAULT NULL, 11 | uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 12 | description TEXT, 13 | is_active BOOLEAN DEFAULT TRUE, 14 | FOREIGN KEY (job_id) REFERENCES jobs(jobID) ON DELETE CASCADE, 15 | FOREIGN KEY (uploaded_by) REFERENCES users(userID) ON DELETE SET NULL, 16 | INDEX idx_job_attachments_job_id (job_id), 17 | INDEX idx_job_attachments_uploaded_at (uploaded_at) 18 | ); -------------------------------------------------------------------------------- /migrations/024_add_pack_workflow.down.sql: -------------------------------------------------------------------------------- 1 | -- Rollback migration 024: Remove pack workflow functionality 2 | 3 | -- Drop view 4 | DROP VIEW IF EXISTS `v_job_pack_progress`; 5 | 6 | -- Drop product_images table 7 | DROP TABLE IF EXISTS `product_images`; 8 | 9 | -- Drop job_device_events table 10 | DROP TABLE IF EXISTS `job_device_events`; 11 | 12 | -- Remove pack workflow columns from jobdevices 13 | ALTER TABLE `jobdevices` 14 | DROP INDEX IF EXISTS `idx_jobdevices_job_pack`, 15 | DROP INDEX IF EXISTS `idx_jobdevices_pack_status`, 16 | DROP COLUMN IF EXISTS `pack_ts`, 17 | DROP COLUMN IF EXISTS `pack_status`; -------------------------------------------------------------------------------- /migrations/024_add_pack_workflow.up.sql: -------------------------------------------------------------------------------- 1 | -- Migration 024: Add pack workflow functionality 2 | -- Adds pack status tracking to jobs and device relationships 3 | 4 | -- Add pack workflow columns to jobdevices table 5 | ALTER TABLE `jobdevices` 6 | ADD COLUMN `pack_status` ENUM('pending','packed','issued','returned') DEFAULT 'pending' NOT NULL AFTER `custom_price`, 7 | ADD COLUMN `pack_ts` DATETIME NULL AFTER `pack_status`, 8 | ADD INDEX `idx_jobdevices_pack_status` (`pack_status`), 9 | ADD INDEX `idx_jobdevices_job_pack` (`jobID`, `pack_status`); 10 | 11 | -- Create job_device_events audit table 12 | CREATE TABLE `job_device_events` ( 13 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 14 | `jobID` INT NOT NULL, 15 | `deviceID` VARCHAR(50) NOT NULL, 16 | `event_type` ENUM('scanned','packed','issued','returned','unpacked') NOT NULL, 17 | `actor` VARCHAR(100) DEFAULT NULL COMMENT 'User who performed the action', 18 | `timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | `metadata` JSON DEFAULT NULL COMMENT 'Additional event data', 20 | PRIMARY KEY (`id`), 21 | KEY `idx_job_device_events_job` (`jobID`), 22 | KEY `idx_job_device_events_device` (`deviceID`), 23 | KEY `idx_job_device_events_type` (`event_type`), 24 | KEY `idx_job_device_events_timestamp` (`timestamp`), 25 | CONSTRAINT `fk_job_device_events_job` FOREIGN KEY (`jobID`) REFERENCES `jobs` (`jobID`) ON DELETE CASCADE, 26 | CONSTRAINT `fk_job_device_events_device` FOREIGN KEY (`deviceID`) REFERENCES `devices` (`deviceID`) ON DELETE CASCADE 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 28 | 29 | -- Create product_images table 30 | CREATE TABLE `product_images` ( 31 | `imageID` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 32 | `productID` INT NOT NULL, 33 | `filename` VARCHAR(255) NOT NULL, 34 | `original_name` VARCHAR(255) DEFAULT NULL, 35 | `file_path` VARCHAR(500) NOT NULL, 36 | `file_size` BIGINT UNSIGNED DEFAULT NULL, 37 | `mime_type` VARCHAR(100) DEFAULT NULL, 38 | `is_primary` BOOLEAN DEFAULT FALSE, 39 | `alt_text` VARCHAR(255) DEFAULT NULL, 40 | `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 41 | `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 42 | PRIMARY KEY (`imageID`), 43 | KEY `idx_product_images_product` (`productID`), 44 | KEY `idx_product_images_primary` (`productID`, `is_primary`), 45 | CONSTRAINT `fk_product_images_product` FOREIGN KEY (`productID`) REFERENCES `products` (`productID`) ON DELETE CASCADE 46 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 47 | 48 | -- Create view for job pack progress 49 | CREATE VIEW `v_job_pack_progress` AS 50 | SELECT 51 | j.jobID, 52 | j.description as job_description, 53 | COUNT(jd.deviceID) as total_devices, 54 | COUNT(CASE WHEN jd.pack_status = 'packed' THEN 1 END) as packed_devices, 55 | COUNT(CASE WHEN jd.pack_status = 'issued' THEN 1 END) as issued_devices, 56 | COUNT(CASE WHEN jd.pack_status = 'returned' THEN 1 END) as returned_devices, 57 | COUNT(CASE WHEN jd.pack_status = 'pending' THEN 1 END) as pending_devices, 58 | CASE 59 | WHEN COUNT(jd.deviceID) = 0 THEN 100.0 60 | ELSE ROUND((COUNT(CASE WHEN jd.pack_status = 'packed' THEN 1 END) * 100.0) / COUNT(jd.deviceID), 2) 61 | END as pack_progress_percent 62 | FROM jobs j 63 | LEFT JOIN jobdevices jd ON j.jobID = jd.jobID 64 | GROUP BY j.jobID, j.description; 65 | 66 | -- Set all existing jobdevices to 'pending' status (they are already there by default) 67 | -- This migration is backward compatible - no data loss -------------------------------------------------------------------------------- /restart-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # TS RentalCore - Development Restart Script 4 | # Kills existing processes and starts with latest code 5 | 6 | set -e 7 | 8 | echo "🔄 Restarting TS RentalCore in Development Mode..." 9 | 10 | # Kill any existing server processes 11 | echo "🛑 Stopping existing server processes..." 12 | pkill -f "go run cmd/server/main.go" || true 13 | pkill -f "./server" || true 14 | fuser -k 8080/tcp || true 15 | 16 | # Give processes time to stop 17 | sleep 2 18 | 19 | # Build and start with latest code 20 | echo "📦 Building and starting with latest code..." 21 | go run cmd/server/main.go -------------------------------------------------------------------------------- /start-production.sh: -------------------------------------------------------------------------------- 1 | #!/run/current-system/sw/bin/bash 2 | 3 | # TS RentalCore - Production Startup Script 4 | # Uses config.json for configuration 5 | 6 | set -e 7 | 8 | echo "Starting TS RentalCore in Production Mode..." 9 | 10 | # Set Go to release mode for better performance 11 | export GIN_MODE=release 12 | 13 | # Create logs directory if it doesn't exist 14 | mkdir -p logs 15 | 16 | # Check if production config file exists 17 | if [ ! -f "config.json" ]; then 18 | echo "Error: config.json not found" 19 | echo "Please ensure the production config file exists in the current directory" 20 | exit 1 21 | fi 22 | 23 | # Always build the latest binary to ensure code changes are included 24 | echo "📦 Building latest binary..." 25 | go build -o server ./cmd/server 26 | echo "✅ Binary built successfully" 27 | 28 | echo "Using configuration file: config.json" 29 | echo "Server will start on port 8080" 30 | echo "Logs will be written to: logs/production.log" 31 | 32 | # Start the application with production config 33 | exec ./server -config=config.json >> logs/production.log 2>&1 34 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # JobScanner Pro - Go Web Application Startup Script 4 | 5 | echo "🚀 Starting JobScanner Pro..." 6 | 7 | # Set up Go environment 8 | export PATH=$PATH:~/go/bin 9 | export GOPATH=~/gopath 10 | 11 | # Check if binary exists, build if not 12 | if [ ! -f "./jobscanner" ]; then 13 | echo "📦 Building application..." 14 | go build -o jobscanner cmd/server/main.go 15 | if [ $? -ne 0 ]; then 16 | echo "❌ Build failed!" 17 | exit 1 18 | fi 19 | echo "✅ Build successful!" 20 | fi 21 | 22 | # Start the application 23 | echo "🌐 Starting web server on http://10.0.0.100:8000" 24 | echo "📊 Dashboard: http://10.0.0.100:8000/jobs" 25 | echo "🔧 Devices: http://10.0.0.100:8000/devices" 26 | echo "👥 Customers: http://10.0.0.100:8000/customers" 27 | echo "" 28 | echo "🌍 External access: http://10.0.0.100:8000" 29 | echo "🏠 Local access: http://localhost:8000" 30 | echo "" 31 | echo "📖 Press Ctrl+C to stop the server" 32 | echo "" 33 | 34 | ./jobscanner -------------------------------------------------------------------------------- /web/scanner/decoder/dedupe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // DedupeCache manages recently decoded barcodes to prevent duplicates 9 | type DedupeCache struct { 10 | cache map[string]*CacheEntry 11 | mutex sync.RWMutex 12 | cooldown time.Duration 13 | } 14 | 15 | // CacheEntry represents a cached decode result 16 | type CacheEntry struct { 17 | Result DecodeResult 18 | Timestamp time.Time 19 | } 20 | 21 | // NewDedupeCache creates a new dedupe cache with specified cooldown 22 | func NewDedupeCache(cooldown time.Duration) *DedupeCache { 23 | if cooldown <= 0 { 24 | cooldown = 2 * time.Second // Default 2 second cooldown 25 | } 26 | 27 | return &DedupeCache{ 28 | cache: make(map[string]*CacheEntry), 29 | cooldown: cooldown, 30 | } 31 | } 32 | 33 | // IsDuplicate checks if a decode result is a recent duplicate 34 | func (dc *DedupeCache) IsDuplicate(result DecodeResult) bool { 35 | dc.mutex.RLock() 36 | defer dc.mutex.RUnlock() 37 | 38 | key := dc.generateKey(result) 39 | entry, exists := dc.cache[key] 40 | 41 | if !exists { 42 | return false 43 | } 44 | 45 | // Check if cooldown period has passed 46 | return time.Since(entry.Timestamp) < dc.cooldown 47 | } 48 | 49 | // Add stores a decode result in the cache 50 | func (dc *DedupeCache) Add(result DecodeResult) { 51 | dc.mutex.Lock() 52 | defer dc.mutex.Unlock() 53 | 54 | key := dc.generateKey(result) 55 | dc.cache[key] = &CacheEntry{ 56 | Result: result, 57 | Timestamp: time.Now(), 58 | } 59 | } 60 | 61 | // Cleanup removes expired entries from the cache 62 | func (dc *DedupeCache) Cleanup() { 63 | dc.mutex.Lock() 64 | defer dc.mutex.Unlock() 65 | 66 | now := time.Now() 67 | for key, entry := range dc.cache { 68 | if now.Sub(entry.Timestamp) > dc.cooldown*2 { // Keep entries for 2x cooldown 69 | delete(dc.cache, key) 70 | } 71 | } 72 | } 73 | 74 | // generateKey creates a unique key for a decode result 75 | func (dc *DedupeCache) generateKey(result DecodeResult) string { 76 | return result.Text + "|" + result.Format 77 | } 78 | 79 | // GetStats returns cache statistics 80 | func (dc *DedupeCache) GetStats() map[string]interface{} { 81 | dc.mutex.RLock() 82 | defer dc.mutex.RUnlock() 83 | 84 | return map[string]interface{}{ 85 | "cacheSize": len(dc.cache), 86 | "cooldownMs": dc.cooldown.Milliseconds(), 87 | } 88 | } 89 | 90 | // Clear removes all entries from the cache 91 | func (dc *DedupeCache) Clear() { 92 | dc.mutex.Lock() 93 | defer dc.mutex.Unlock() 94 | 95 | dc.cache = make(map[string]*CacheEntry) 96 | } -------------------------------------------------------------------------------- /web/scanner/decoder/roi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | // extractROI extracts a region of interest from the image 9 | func extractROI(img image.Image, roi *ROI) (image.Image, error) { 10 | if roi == nil { 11 | return img, nil 12 | } 13 | 14 | bounds := img.Bounds() 15 | 16 | // Validate ROI bounds 17 | if roi.X < 0 || roi.Y < 0 || 18 | roi.X+roi.Width > bounds.Max.X || 19 | roi.Y+roi.Height > bounds.Max.Y { 20 | return nil, ErrInvalidROI 21 | } 22 | 23 | // Create a new image with the ROI 24 | roiImg := image.NewRGBA(image.Rect(0, 0, roi.Width, roi.Height)) 25 | 26 | for y := 0; y < roi.Height; y++ { 27 | for x := 0; x < roi.Width; x++ { 28 | roiImg.Set(x, y, img.At(roi.X+x, roi.Y+y)) 29 | } 30 | } 31 | 32 | return roiImg, nil 33 | } 34 | 35 | // createCenterROI creates a center ROI for 1D barcode priority scanning 36 | func createCenterROI(width, height int, percentage float64) *ROI { 37 | if percentage <= 0 || percentage >= 1 { 38 | percentage = 0.7 // Default 70% center area 39 | } 40 | 41 | roiWidth := int(float64(width) * percentage) 42 | roiHeight := int(float64(height) * percentage) 43 | 44 | x := (width - roiWidth) / 2 45 | y := (height - roiHeight) / 2 46 | 47 | return &ROI{ 48 | X: x, 49 | Y: y, 50 | Width: roiWidth, 51 | Height: roiHeight, 52 | } 53 | } 54 | 55 | // preprocessImage applies basic image preprocessing for better decode rates 56 | func preprocessImage(img image.Image) image.Image { 57 | bounds := img.Bounds() 58 | processed := image.NewGray(bounds) 59 | 60 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 61 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 62 | originalColor := img.At(x, y) 63 | grayColor := color.GrayModel.Convert(originalColor).(color.Gray) 64 | processed.Set(x, y, grayColor) 65 | } 66 | } 67 | 68 | return processed 69 | } 70 | 71 | // rgbaToImage converts RGBA byte slice to image.Image 72 | func rgbaToImage(data []byte, width, height int) (image.Image, error) { 73 | if len(data) != width*height*4 { 74 | return nil, ErrInvalidImage 75 | } 76 | 77 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 78 | 79 | for y := 0; y < height; y++ { 80 | for x := 0; x < width; x++ { 81 | idx := (y*width + x) * 4 82 | r := data[idx] 83 | g := data[idx+1] 84 | b := data[idx+2] 85 | a := data[idx+3] 86 | 87 | img.Set(x, y, color.RGBA{r, g, b, a}) 88 | } 89 | } 90 | 91 | return img, nil 92 | } -------------------------------------------------------------------------------- /web/scanner/decoder/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Supported barcode formats (currently available in gozxing v0.1.1) 8 | type BarcodeFormat int 9 | 10 | const ( 11 | CODE128 BarcodeFormat = iota 12 | CODE39 13 | EAN13 14 | EAN8 15 | UPCA 16 | UPCE 17 | ITF 18 | QR_CODE 19 | // Note: DATA_MATRIX and PDF417 not available in current gozxing version 20 | ) 21 | 22 | var formatNames = map[BarcodeFormat]string{ 23 | CODE128: "CODE_128", 24 | CODE39: "CODE_39", 25 | EAN13: "EAN_13", 26 | EAN8: "EAN_8", 27 | UPCA: "UPC_A", 28 | UPCE: "UPC_E", 29 | ITF: "ITF", 30 | QR_CODE: "QR_CODE", 31 | } 32 | 33 | func (f BarcodeFormat) String() string { 34 | if name, ok := formatNames[f]; ok { 35 | return name 36 | } 37 | return "UNKNOWN" 38 | } 39 | 40 | // DecodeResult represents the result of a barcode decode operation 41 | type DecodeResult struct { 42 | Text string `json:"text"` 43 | Format string `json:"format"` 44 | CornerPoints []Point `json:"cornerPoints"` 45 | Confidence float64 `json:"confidence"` 46 | Timestamp int64 `json:"timestamp"` 47 | } 48 | 49 | // Point represents a 2D coordinate 50 | type Point struct { 51 | X float64 `json:"x"` 52 | Y float64 `json:"y"` 53 | } 54 | 55 | // ROI represents a region of interest for focused scanning 56 | type ROI struct { 57 | X int `json:"x"` 58 | Y int `json:"y"` 59 | Width int `json:"width"` 60 | Height int `json:"height"` 61 | } 62 | 63 | // ScanPriority indicates whether to prioritize 1D or 2D barcodes 64 | type ScanPriority int 65 | 66 | const ( 67 | Priority1D ScanPriority = iota 68 | Priority2D 69 | PriorityAuto 70 | ) 71 | 72 | // DecoderConfig holds configuration for the decoder 73 | type DecoderConfig struct { 74 | EnabledFormats []BarcodeFormat `json:"enabledFormats"` 75 | Priority ScanPriority `json:"priority"` 76 | ROI *ROI `json:"roi,omitempty"` 77 | MaxRetries int `json:"maxRetries"` 78 | Timeout int `json:"timeout"` // milliseconds 79 | } 80 | 81 | // Standard errors 82 | var ( 83 | ErrNoCodeFound = errors.New("no barcode found") 84 | ErrInvalidImage = errors.New("invalid image data") 85 | ErrUnsupportedFmt = errors.New("unsupported format") 86 | ErrTimeout = errors.New("decode timeout") 87 | ErrInvalidROI = errors.New("invalid ROI") 88 | ) 89 | 90 | // Default configuration for industrial scanning 91 | func DefaultConfig() DecoderConfig { 92 | return DecoderConfig{ 93 | EnabledFormats: []BarcodeFormat{ 94 | CODE128, CODE39, EAN13, EAN8, UPCA, UPCE, ITF, QR_CODE, 95 | // Note: DATA_MATRIX and PDF417 not available in current gozxing version 96 | }, 97 | Priority: PriorityAuto, 98 | MaxRetries: 3, 99 | Timeout: 100, // 100ms per decode attempt 100 | } 101 | } -------------------------------------------------------------------------------- /web/scanner/wasm/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for building the Go WASM decoder 2 | 3 | .PHONY: build clean wasm-exec 4 | 5 | # Build the WASM decoder 6 | build: wasm-exec 7 | @echo "Building Go WASM decoder..." 8 | cd ../decoder && \ 9 | GOOS=js GOARCH=wasm go build -o ../wasm/decoder.wasm . 10 | @echo "✅ WASM decoder built successfully: web/scanner/wasm/decoder.wasm" 11 | 12 | # Copy wasm_exec.js from Go toolchain 13 | wasm-exec: 14 | @echo "Copying wasm_exec.js from Go toolchain..." 15 | cp "$$(go env GOROOT)/misc/wasm/wasm_exec.js" ./wasm_exec.js 16 | @echo "✅ wasm_exec.js copied" 17 | 18 | # Clean build artifacts 19 | clean: 20 | @echo "Cleaning build artifacts..." 21 | rm -f decoder.wasm wasm_exec.js 22 | @echo "✅ Build artifacts cleaned" 23 | 24 | # Build with optimization flags 25 | build-optimized: wasm-exec 26 | @echo "Building optimized Go WASM decoder..." 27 | cd ../decoder && \ 28 | GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o ../wasm/decoder.wasm . 29 | @echo "✅ Optimized WASM decoder built: web/scanner/wasm/decoder.wasm" 30 | 31 | # Verify the build 32 | verify: 33 | @echo "Verifying WASM build..." 34 | @if [ -f decoder.wasm ]; then \ 35 | echo "✅ decoder.wasm exists"; \ 36 | ls -lh decoder.wasm; \ 37 | else \ 38 | echo "❌ decoder.wasm not found"; \ 39 | exit 1; \ 40 | fi 41 | @if [ -f wasm_exec.js ]; then \ 42 | echo "✅ wasm_exec.js exists"; \ 43 | else \ 44 | echo "❌ wasm_exec.js not found"; \ 45 | exit 1; \ 46 | fi 47 | 48 | # Development build (with debug info) 49 | dev: build verify 50 | 51 | # Production build (optimized) 52 | prod: build-optimized verify 53 | 54 | # Help 55 | help: 56 | @echo "Available targets:" 57 | @echo " build - Build WASM decoder" 58 | @echo " build-optimized- Build optimized WASM decoder" 59 | @echo " clean - Clean build artifacts" 60 | @echo " verify - Verify build output" 61 | @echo " dev - Development build with verification" 62 | @echo " prod - Production build with verification" 63 | @echo " help - Show this help" -------------------------------------------------------------------------------- /web/scanner/wasm/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build script for Go WASM decoder 4 | set -e 5 | 6 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | DECODER_DIR="$SCRIPT_DIR/../decoder" 8 | OUTPUT_DIR="$SCRIPT_DIR" 9 | 10 | echo "🔧 Building Go WASM decoder..." 11 | 12 | # Ensure we're in the right directory 13 | cd "$SCRIPT_DIR" 14 | 15 | # Copy wasm_exec.js if it doesn't exist or is outdated 16 | WASM_EXEC_PATH="$(go env GOROOT)/lib/wasm/wasm_exec.js" 17 | if [ ! -f "$WASM_EXEC_PATH" ]; then 18 | # Try alternative path 19 | WASM_EXEC_PATH="$(go env GOROOT)/misc/wasm/wasm_exec.js" 20 | fi 21 | 22 | if [ ! -f "wasm_exec.js" ] || [ "$WASM_EXEC_PATH" -nt "wasm_exec.js" ]; then 23 | echo "📦 Copying wasm_exec.js from Go toolchain..." 24 | cp "$WASM_EXEC_PATH" ./wasm_exec.js 25 | echo "✅ wasm_exec.js updated" 26 | fi 27 | 28 | # Build the WASM module 29 | echo "🏗️ Compiling Go to WASM..." 30 | cd "$DECODER_DIR" 31 | 32 | # Use optimization flags for production builds 33 | if [ "$1" = "prod" ] || [ "$1" = "production" ]; then 34 | echo "🚀 Building production version (optimized)..." 35 | GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o "$OUTPUT_DIR/decoder.wasm" . 36 | else 37 | echo "🛠️ Building development version..." 38 | GOOS=js GOARCH=wasm go build -o "$OUTPUT_DIR/decoder.wasm" . 39 | fi 40 | 41 | cd "$OUTPUT_DIR" 42 | 43 | # Verify the build 44 | if [ -f "decoder.wasm" ]; then 45 | SIZE=$(du -h decoder.wasm | cut -f1) 46 | echo "✅ WASM decoder built successfully!" 47 | echo "📦 Size: $SIZE" 48 | echo "📍 Location: web/scanner/wasm/decoder.wasm" 49 | else 50 | echo "❌ Build failed - decoder.wasm not found" 51 | exit 1 52 | fi 53 | 54 | # Optional: Run basic verification 55 | if command -v file >/dev/null 2>&1; then 56 | FILE_TYPE=$(file decoder.wasm) 57 | echo "🔍 File type: $FILE_TYPE" 58 | fi 59 | 60 | echo "🎉 Build complete!" 61 | echo "" 62 | echo "Next steps:" 63 | echo "1. Ensure the WASM files are served by your web server" 64 | echo "2. Load the decoder in a Web Worker" 65 | echo "3. Test with the scanner interface" -------------------------------------------------------------------------------- /web/scanner/wasm/decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/web/scanner/wasm/decoder.wasm -------------------------------------------------------------------------------- /web/static/images/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/web/static/images/icon-180.png -------------------------------------------------------------------------------- /web/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RentalCore - Professional Equipment Management", 3 | "short_name": "RentalCore", 4 | "description": "The core of your rental business - comprehensive equipment and operations management with professional theme", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#0f172a", 8 | "theme_color": "#1e293b", 9 | "orientation": "any", 10 | "scope": "/", 11 | "lang": "en", 12 | "dir": "ltr", 13 | "categories": ["business", "productivity", "utilities", "finance"], 14 | "prefer_related_applications": false, 15 | "icons": [ 16 | { 17 | "src": "/static/images/icon-72.png", 18 | "sizes": "72x72", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/static/images/icon-96.png", 23 | "sizes": "96x96", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "/static/images/icon-128.png", 28 | "sizes": "128x128", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "/static/images/icon-144.png", 33 | "sizes": "144x144", 34 | "type": "image/png" 35 | }, 36 | { 37 | "src": "/static/images/icon-152.png", 38 | "sizes": "152x152", 39 | "type": "image/png" 40 | }, 41 | { 42 | "src": "/static/images/icon-192.png", 43 | "sizes": "192x192", 44 | "type": "image/png", 45 | "purpose": "any maskable" 46 | }, 47 | { 48 | "src": "/static/images/icon-384.png", 49 | "sizes": "384x384", 50 | "type": "image/png" 51 | }, 52 | { 53 | "src": "/static/images/icon-512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "any maskable" 57 | } 58 | ], 59 | "shortcuts": [ 60 | { 61 | "name": "Analytics Dashboard", 62 | "short_name": "Analytics", 63 | "description": "View business analytics", 64 | "url": "/analytics", 65 | "icons": [ 66 | { 67 | "src": "/static/images/icon-96.png", 68 | "sizes": "96x96" 69 | } 70 | ] 71 | }, 72 | { 73 | "name": "Start Scanning", 74 | "short_name": "Scan", 75 | "description": "Start barcode scanning", 76 | "url": "/scan/select", 77 | "icons": [ 78 | { 79 | "src": "/static/images/icon-96.png", 80 | "sizes": "96x96" 81 | } 82 | ] 83 | }, 84 | { 85 | "name": "Enhanced Scanner", 86 | "short_name": "Enhanced Scan", 87 | "description": "Advanced mobile scanner with vibration, zoom, and focus", 88 | "url": "/scan/select", 89 | "icons": [ 90 | { 91 | "src": "/static/images/icon-96.png", 92 | "sizes": "96x96" 93 | } 94 | ] 95 | }, 96 | { 97 | "name": "Search Everything", 98 | "short_name": "Search", 99 | "description": "Search across all entities", 100 | "url": "/search/global", 101 | "icons": [ 102 | { 103 | "src": "/static/images/icon-96.png", 104 | "sizes": "96x96" 105 | } 106 | ] 107 | }, 108 | { 109 | "name": "View Jobs", 110 | "short_name": "Jobs", 111 | "description": "View all jobs", 112 | "url": "/jobs", 113 | "icons": [ 114 | { 115 | "src": "/static/images/icon-96.png", 116 | "sizes": "96x96" 117 | } 118 | ] 119 | } 120 | ], 121 | "screenshots": [ 122 | { 123 | "src": "/static/images/screenshot-narrow.png", 124 | "sizes": "540x720", 125 | "type": "image/png", 126 | "form_factor": "narrow" 127 | }, 128 | { 129 | "src": "/static/images/screenshot-wide.png", 130 | "sizes": "1280x720", 131 | "type": "image/png", 132 | "form_factor": "wide" 133 | } 134 | ] 135 | } -------------------------------------------------------------------------------- /web/static/scanner/wasm/decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbt4/rentalcore/3f6117a2823417ed2eb214ed1d05176efa7a78ea/web/static/scanner/wasm/decoder.wasm -------------------------------------------------------------------------------- /web/templates/cable_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | {{.title}} - RentalCore 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{template "navbar.html" .}} 22 | 23 | 24 |
25 | 26 |
27 |
28 |

{{.title}}

29 |

Add a new cable to your inventory

30 |
31 | 36 |
37 | 38 | 39 |
40 |
41 | {{if .error}} 42 |
43 | 44 | {{.error}} 45 |
46 | {{end}} 47 | 48 |
49 |
50 | 51 |
52 | 53 | 59 |
60 | 61 | 62 |
63 | 64 | 66 |
67 | 68 | 69 |
70 | 71 | 79 |
80 | 81 | 82 |
83 | 84 | 92 |
93 | 94 | 95 |
96 | 97 | 99 |
100 | 101 | 102 |
103 | 104 | 106 | Number of identical cables to create 107 |
108 |
109 | 110 | 111 |
112 | 115 | 116 | Cancel 117 | 118 |
119 |
120 |
121 |
122 |
123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /web/templates/customer_detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | Customer Details - RentalCore 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 57 | 58 |
59 |
60 |

Customer Details

61 |
62 | 63 | Edit 64 | 65 | 66 | Back 67 | 68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |
Customer Information
76 |
77 |
78 |
79 |
80 |
Basic Information
81 | {{if .customer.CompanyName}} 82 |

Company: {{.customer.CompanyName}}

83 | {{end}} 84 | {{if .customer.FirstName}} 85 |

Name: {{.customer.FirstName}} {{.customer.LastName}}

86 | {{end}} 87 |

Customer Type: {{.customer.CustomerType}}

88 | 89 |
Contact Information
90 | {{if .customer.Email}} 91 |

Email: {{.customer.Email}}

92 | {{end}} 93 | {{if .customer.PhoneNumber}} 94 |

Phone: {{.customer.PhoneNumber}}

95 | {{end}} 96 |
97 |
98 |
Address
99 | {{if .customer.Street}} 100 |

101 | {{.customer.Street}}{{if .customer.HouseNumber}} {{.customer.HouseNumber}}{{end}}
102 | {{if .customer.ZIP}}{{.customer.ZIP}} {{end}}{{.customer.City}}
103 | {{if .customer.FederalState}}{{.customer.FederalState}}
{{end}} 104 | {{.customer.Country}} 105 |

106 | {{end}} 107 | 108 | {{if .customer.Notes}} 109 |
Notes
110 |

{{.customer.Notes}}

111 | {{end}} 112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
Quick Actions
121 |
122 | 132 |
133 |
134 |
135 |
136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /web/templates/device_detail.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 |
4 | 10 |

{{.device.Name}} {{.device.SerialNo}}

11 |
12 | 23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 |
Device Information
31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 |
Serial Number:{{.device.SerialNo}}
Category:{{.device.Category}}
Price:€{{printf "%.2f" .device.Price}}
Available: 49 | {{if .device.Available}} 50 | Yes 51 | {{else}} 52 | No 53 | {{end}} 54 |
Created:{{.device.CreatedAt.Format "2006-01-02 15:04"}}
61 | {{if .device.Description}} 62 |
63 | Description: 64 |

{{.device.Description}}

65 |
66 | {{end}} 67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 |
Job History
76 |
77 |
78 | {{if .history}} 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {{range .history}} 91 | 92 | 97 | 98 | 99 | 106 | 107 | {{end}} 108 | 109 |
JobCustomerAssignedStatus
93 | 94 | {{.Job.Title}} 95 | 96 | {{.Job.Customer.Name}}{{.AssignedAt.Format "2006-01-02"}} 100 | {{if .RemovedAt}} 101 | Completed 102 | {{else}} 103 | Active 104 | {{end}} 105 |
110 |
111 | {{else}} 112 |
113 | 114 |

No job history available

115 |
116 | {{end}} 117 |
118 |
119 |
120 |
121 | {{end}} -------------------------------------------------------------------------------- /web/templates/error.html: -------------------------------------------------------------------------------- 1 | {{template "base.html" .}} 2 | {{define "content"}} 3 |
4 |
5 |
6 |
7 |
Error
8 |
9 |
10 |
11 |
Something went wrong:
12 |

{{.error}}

13 |
14 | 15 |
16 | 19 | 20 | Home 21 | 22 |
23 |
24 |
25 |
26 |
27 | {{end}} -------------------------------------------------------------------------------- /web/templates/error_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | Error {{.error_code}} - RentalCore 14 | 15 | 16 | 17 | 18 | 19 | 20 | 116 | 117 | 118 |
119 |
120 |
121 | 122 |
123 | 124 |
125 | Error {{.error_code}} 126 |
127 | 128 |
129 | {{.error_message}} 130 |
131 | 132 | {{if .error_details}} 133 |
134 | Technical Details:
135 | {{.error_details}} 136 |
137 | {{end}} 138 | 139 |
140 | 143 | 144 | Home 145 | 146 | 149 |
150 | 151 | {{if .request_id}} 152 |
153 | Request ID: {{.request_id}}
154 | Time: {{.timestamp}} 155 |
156 | {{end}} 157 |
158 |
159 | 160 | 170 | 171 | -------------------------------------------------------------------------------- /web/templates/error_standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | Error - RentalCore 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 |

{{if .title}}{{.title}}{{else}}Error{{end}}

55 | 56 |

{{if .error}}{{.error}}{{else}}Page not found{{end}}

57 | 58 |
59 | 62 | 63 | Home 64 | 65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 | RentalCore - Advanced Equipment Management System 77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | --------------------------------------------------------------------------------