bb44b143ec
- 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
718 lines
20 KiB
Markdown
718 lines
20 KiB
Markdown
# 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. |