Initial commit: MinIO WebUI - Complete implementation
- Backend: Express.js API with MinIO CLI integration - Frontend: React with Material-UI for non-technical users - Features: Bucket management, user creation, storage monitoring - Security: JWT auth, IP filtering, encrypted passwords - Docker support for easy deployment - Automated weekly storage reports - Setup and deployment scripts included
This commit is contained in:
@@ -0,0 +1,718 @@
|
||||
# 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 (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Create New Bucket with User</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stepper activeStep={activeStep} sx={{ mb: 3 }}>
|
||||
<Step><StepLabel>Bucket Details</StepLabel></Step>
|
||||
<Step><StepLabel>User Credentials</StepLabel></Step>
|
||||
<Step><StepLabel>Complete</StepLabel></Step>
|
||||
</Stepper>
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{activeStep === 0 && (
|
||||
<>
|
||||
<TextField
|
||||
{...register('bucketName')}
|
||||
label="Bucket Name"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
error={!!errors.bucketName}
|
||||
helperText={errors.bucketName?.message || 'e.g., alice-bucket'}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setActiveStep(1)}
|
||||
disabled={!!errors.bucketName}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeStep === 1 && (
|
||||
<>
|
||||
<TextField
|
||||
{...register('username')}
|
||||
label="Username"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
error={!!errors.username}
|
||||
helperText={errors.username?.message}
|
||||
/>
|
||||
<TextField
|
||||
{...register('password')}
|
||||
label="Password"
|
||||
type="password"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
<Button onClick={() => setActiveStep(0)} sx={{ mt: 2, mr: 1 }}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : 'Create'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeStep === 2 && (
|
||||
<Alert severity="success">
|
||||
Bucket and user created successfully!
|
||||
</Alert>
|
||||
)}
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 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<any[]>([]);
|
||||
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 (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Storage Overview
|
||||
</Typography>
|
||||
|
||||
{loading ? (
|
||||
<LinearProgress />
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
<Card sx={{ flex: 1 }}>
|
||||
<CardContent>
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
Total Storage Used
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{reportService.formatBytes(totalSize)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card sx={{ flex: 1 }}>
|
||||
<CardContent>
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
Number of Buckets
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{bucketSizes.length}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Card sx={{ flex: 2 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Bucket Details
|
||||
</Typography>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bucket Name</TableCell>
|
||||
<TableCell align="right">Size</TableCell>
|
||||
<TableCell align="right">Objects</TableCell>
|
||||
<TableCell>Last Modified</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{bucketSizes.map((bucket) => (
|
||||
<TableRow key={bucket.name}>
|
||||
<TableCell>{bucket.name}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Chip label={bucket.sizeFormatted} size="small" />
|
||||
</TableCell>
|
||||
<TableCell align="right">{bucket.objectCount}</TableCell>
|
||||
<TableCell>{bucket.lastModified}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card sx={{ flex: 1 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Storage Distribution
|
||||
</Typography>
|
||||
<Box sx={{ height: 300 }}>
|
||||
<Pie data={chartData} options={{ maintainAspectRatio: false }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user