# MinIO WebUI - Technical Implementation Guide ## Quick Start Implementation ### 1. Backend Service Implementation #### MinIO Service Layer ```javascript // backend/src/services/minio.service.js const { exec } = require('child_process'); const util = require('util'); const execAsync = util.promisify(exec); class MinIOService { constructor(alias = process.env.DEFAULT_MINIO_ALIAS) { this.alias = alias; } // Create bucket with user and policy (based on kopia-user-create.sh) async createBucketWithUser(bucketName, username, password) { try { // Create bucket await execAsync(`mc mb ${this.alias}/${bucketName}`); // Create user await execAsync(`mc admin user add ${this.alias} ${username} ${password}`); // Create policy const policyName = `${username}-policy`; const policy = { Version: "2012-10-17", Statement: [{ Effect: "Allow", Action: ["s3:*"], Resource: [ `arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*` ] }] }; const policyFile = `/tmp/${policyName}.json`; require('fs').writeFileSync(policyFile, JSON.stringify(policy)); await execAsync(`mc admin policy create ${this.alias} ${policyName} ${policyFile}`); await execAsync(`mc admin policy attach ${this.alias} ${policyName} --user ${username}`); // Cleanup require('fs').unlinkSync(policyFile); return { bucketName, username, policyName }; } catch (error) { throw new Error(`Failed to create bucket with user: ${error.message}`); } } // Get bucket sizes for invoicing (based on speicherauswertung_mail.sh) async getBucketSizes() { try { const { stdout: bucketsOutput } = await execAsync(`mc ls ${this.alias} --json`); const buckets = bucketsOutput.split('\n').filter(line => line).map(line => JSON.parse(line)); const bucketSizes = await Promise.all( buckets.map(async (bucket) => { const { stdout: sizeOutput } = await execAsync(`mc du --json ${this.alias}/${bucket.key}`); const sizeInfo = JSON.parse(sizeOutput); // Get last modified const { stdout: lastModified } = await execAsync( `mc find ${this.alias}/${bucket.key} --json --print '{time}' | sort | tail -n1` ); return { name: bucket.key, size: sizeInfo.size, sizeFormatted: this.formatBytes(sizeInfo.size), lastModified: lastModified.trim() || 'No files', objectCount: sizeInfo.objects || 0 }; }) ); return bucketSizes; } catch (error) { throw new Error(`Failed to get bucket sizes: ${error.message}`); } } formatBytes(bytes) { const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 B'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`; } } module.exports = MinIOService; ``` #### Authentication Middleware ```javascript // backend/src/middleware/auth.middleware.js const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const authMiddleware = async (req, res, next) => { try { const token = req.cookies.token || req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Authentication required' }); } const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (error) { return res.status(401).json({ error: 'Invalid or expired token' }); } }; module.exports = authMiddleware; ``` #### IP Filter Middleware ```javascript // backend/src/middleware/ipFilter.middleware.js const ipRangeCheck = require('ip-range-check'); const ipFilterMiddleware = (req, res, next) => { if (process.env.ENABLE_IP_RESTRICTION !== 'true') { return next(); } const clientIp = req.ip || req.connection.remoteAddress; const allowedIps = process.env.ALLOWED_IPS.split(',').map(ip => ip.trim()); if (ipRangeCheck(clientIp, allowedIps)) { next(); } else { console.warn(`Unauthorized access attempt from IP: ${clientIp}`); res.status(403).json({ error: 'Access denied' }); } }; module.exports = ipFilterMiddleware; ``` ### 2. Frontend Implementation #### Bucket Creation Component ```tsx // frontend/src/components/Buckets/CreateBucketDialog.tsx import React, { useState } from 'react'; import { Dialog, DialogTitle, DialogContent, TextField, Button, Stepper, Step, StepLabel, Alert, CircularProgress } from '@mui/material'; import { useForm } from 'react-hook-form'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; import { bucketService } from '../../services/bucket.service'; const schema = yup.object({ bucketName: yup.string() .required('Bucket name is required') .matches(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, 'Invalid bucket name format') .min(3, 'Minimum 3 characters') .max(63, 'Maximum 63 characters'), username: yup.string() .required('Username is required') .matches(/^[a-zA-Z0-9_-]+$/, 'Only alphanumeric, hyphen and underscore allowed'), password: yup.string() .required('Password is required') .min(8, 'Minimum 8 characters') .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Must contain uppercase, lowercase and number') }); export const CreateBucketDialog: React.FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => { const [activeStep, setActiveStep] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const { register, handleSubmit, formState: { errors }, reset } = useForm({ resolver: yupResolver(schema) }); const onSubmit = async (data: any) => { try { setLoading(true); setError(''); await bucketService.createBucketWithUser(data.bucketName, data.username, data.password); setActiveStep(2); setTimeout(() => { reset(); onClose(); setActiveStep(0); }, 2000); } catch (err: any) { setError(err.response?.data?.error || 'Failed to create bucket'); } finally { setLoading(false); } }; return ( Create New Bucket with User Bucket Details User Credentials Complete {error && {error}}
{activeStep === 0 && ( <> )} {activeStep === 1 && ( <> )} {activeStep === 2 && ( Bucket and user created successfully! )}
); }; ``` #### Storage Dashboard Component ```tsx // frontend/src/components/Reports/StorageDashboard.tsx import React, { useEffect, useState } from 'react'; import { Card, CardContent, Typography, Table, TableBody, TableCell, TableHead, TableRow, Chip, Box, LinearProgress } from '@mui/material'; import { Pie } from 'react-chartjs-2'; import { reportService } from '../../services/report.service'; export const StorageDashboard: React.FC = () => { const [bucketSizes, setBucketSizes] = useState([]); const [loading, setLoading] = useState(true); const [totalSize, setTotalSize] = useState(0); useEffect(() => { loadBucketSizes(); const interval = setInterval(loadBucketSizes, 60000); // Refresh every minute return () => clearInterval(interval); }, []); const loadBucketSizes = async () => { try { const data = await reportService.getBucketSizes(); setBucketSizes(data); setTotalSize(data.reduce((sum, bucket) => sum + bucket.size, 0)); } catch (error) { console.error('Failed to load bucket sizes:', error); } finally { setLoading(false); } }; const chartData = { labels: bucketSizes.map(b => b.name), datasets: [{ data: bucketSizes.map(b => b.size), backgroundColor: [ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40' ] }] }; return ( Storage Overview {loading ? ( ) : ( <> Total Storage Used {reportService.formatBytes(totalSize)} Number of Buckets {bucketSizes.length} Bucket Details Bucket Name Size Objects Last Modified {bucketSizes.map((bucket) => ( {bucket.name} {bucket.objectCount} {bucket.lastModified} ))}
Storage Distribution
)}
); }; ``` ### 3. Environment Setup #### .env.example ```bash # Application NODE_ENV=production PORT=3000 LOG_LEVEL=info # Authentication ADMIN_PASSWORD_HASH=$2b$12$abcdefghijklmnopqrstuvwxyz123456789 JWT_SECRET=your-super-secret-jwt-key-minimum-64-characters-long-random-string SESSION_TIMEOUT=1800 # IP Restrictions ENABLE_IP_RESTRICTION=true ALLOWED_IPS=192.168.1.0/24,10.0.0.5,172.16.0.0/16 # MinIO Configuration DEFAULT_MINIO_ALIAS=kopiaminio MINIO_ENDPOINT=https://minio.example.com MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin # Email Configuration (for reports) SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=your-email@gmail.com SMTP_PASS=your-app-password REPORT_RECIPIENT=info@example.com REPORT_SENDER=kopiabackup@example.com # Report Schedule (cron format) REPORT_SCHEDULE=0 0 * * 1 # Every Monday at midnight ``` ### 4. Docker Deployment #### docker-compose.yml ```yaml version: '3.8' services: backend: build: context: ./backend dockerfile: ../docker/Dockerfile.backend environment: - NODE_ENV=production env_file: - .env ports: - "3000:3000" restart: unless-stopped depends_on: - redis volumes: - ./logs:/app/logs - ~/.mc:/root/.mc:ro # MinIO client config frontend: build: context: ./frontend dockerfile: ../docker/Dockerfile.frontend ports: - "80:80" - "443:443" restart: unless-stopped depends_on: - backend volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf - ./ssl:/etc/nginx/ssl redis: image: redis:7-alpine restart: unless-stopped volumes: - redis-data:/data volumes: redis-data: ``` #### Nginx Configuration ```nginx # nginx/default.conf server { listen 80; server_name _; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name _; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # IP Restriction (backup for application-level restriction) allow 192.168.1.0/24; allow 10.0.0.5; allow 172.16.0.0/16; deny all; location / { root /usr/share/nginx/html; try_files $uri /index.html; } location /api { proxy_pass http://backend:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } } ``` ### 5. Security Implementation Checklist - [ ] Implement bcrypt password hashing with salt rounds >= 12 - [ ] Use secure session cookies (HttpOnly, Secure, SameSite=Strict) - [ ] Implement CSRF protection using double-submit cookies - [ ] Add rate limiting (express-rate-limit) - [ ] Input validation on all API endpoints - [ ] Escape shell commands to prevent injection - [ ] Implement proper error handling without exposing internals - [ ] Add security headers (Helmet.js) - [ ] Regular dependency updates (npm audit) - [ ] Implement audit logging for all operations - [ ] Use HTTPS everywhere - [ ] Implement proper CORS configuration - [ ] Add Content Security Policy - [ ] Implement request size limits - [ ] Add API versioning ### 6. Automated Report Service ```javascript // backend/src/services/report.service.js const cron = require('node-cron'); const nodemailer = require('nodemailer'); const MinIOService = require('./minio.service'); class ReportService { constructor() { this.minioService = new MinIOService(); this.setupSchedule(); } setupSchedule() { const schedule = process.env.REPORT_SCHEDULE || '0 0 * * 1'; cron.schedule(schedule, async () => { console.log('Running scheduled storage report...'); await this.generateAndSendReport(); }); } async generateAndSendReport() { try { const bucketSizes = await this.minioService.getBucketSizes(); const users = await this.minioService.listUsers(); const report = this.formatReport(bucketSizes, users); await this.sendEmail(report); console.log('Storage report sent successfully'); } catch (error) { console.error('Failed to generate report:', error); } } formatReport(bucketSizes, users) { const date = new Date().toISOString().split('T')[0]; const totalSize = bucketSizes.reduce((sum, b) => sum + b.size, 0); let report = `MinIO Speicherauswertung\n`; report += `Datum: ${date}\n`; report += `----------------------------------------\n\n`; report += `Alle MinIO-User:\n`; users.forEach(user => { report += `- ${user}\n`; }); report += `\nAlle Buckets:\n`; bucketSizes.forEach(bucket => { report += `\nBucket: ${bucket.name}\n`; report += ` Größe: ${bucket.sizeFormatted}\n`; report += ` Letzte Dateiänderung: ${bucket.lastModified}\n`; }); report += `\n----------------------------------------\n`; report += `Gesamtspeicher: ${this.minioService.formatBytes(totalSize)}\n`; return report; } async sendEmail(content) { const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: process.env.SMTP_PORT, secure: process.env.SMTP_SECURE === 'true', auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } }); await transporter.sendMail({ from: process.env.REPORT_SENDER, to: process.env.REPORT_RECIPIENT, subject: `MinIO Speicherauswertung ${new Date().toISOString().split('T')[0]}`, text: content }); } } module.exports = ReportService; ``` ### 7. Quick Setup Script ```bash #!/bin/bash # scripts/setup.sh echo "MinIO WebUI Setup" echo "=================" # Check prerequisites command -v node >/dev/null 2>&1 || { echo "Node.js is required but not installed."; exit 1; } command -v mc >/dev/null 2>&1 || { echo "MinIO Client (mc) is required but not installed."; exit 1; } # Create .env file if [ ! -f .env ]; then cp .env.example .env echo "Created .env file. Please configure it before starting." fi # Generate secure passwords echo "Generating secure passwords..." ADMIN_PASS=$(openssl rand -base64 32) JWT_SECRET=$(openssl rand -base64 64) echo "Admin Password: $ADMIN_PASS" echo "Hashing password..." ADMIN_HASH=$(node -e "const bcrypt = require('bcrypt'); console.log(bcrypt.hashSync('$ADMIN_PASS', 12));") # Update .env sed -i "s|ADMIN_PASSWORD_HASH=.*|ADMIN_PASSWORD_HASH=$ADMIN_HASH|" .env sed -i "s|JWT_SECRET=.*|JWT_SECRET=$JWT_SECRET|" .env # Install dependencies echo "Installing backend dependencies..." cd backend && npm install echo "Installing frontend dependencies..." cd ../frontend && npm install # Build frontend echo "Building frontend..." npm run build echo "Setup complete! Don't forget to:" echo "1. Configure MinIO connection in .env" echo "2. Set allowed IPs in .env" echo "3. Configure email settings for reports" echo "" echo "To start: docker-compose up -d" ``` This implementation guide provides a solid foundation for building the MinIO WebUI with all the requested features while maintaining security and simplicity for non-technical users.