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,69 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const config = require('../config');
|
||||
const { logger } = require('../utils/logger');
|
||||
const { AppError } = require('../middleware/errorHandler.middleware');
|
||||
|
||||
class AuthService {
|
||||
generateToken(payload) {
|
||||
return jwt.sign(
|
||||
payload,
|
||||
config.auth.jwtSecret,
|
||||
{ expiresIn: config.auth.sessionTimeout }
|
||||
);
|
||||
}
|
||||
|
||||
verifyToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, config.auth.jwtSecret);
|
||||
} catch (error) {
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
throw new AppError('Session expired', 401);
|
||||
}
|
||||
throw new AppError('Invalid token', 401);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPassword(password) {
|
||||
try {
|
||||
const isValid = await bcrypt.compare(password, config.auth.adminPasswordHash);
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
logger.error('Password verification error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hashPassword(password) {
|
||||
const saltRounds = 12;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
createSession(ip) {
|
||||
const sessionData = {
|
||||
role: 'admin',
|
||||
ip,
|
||||
loginTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const token = this.generateToken(sessionData);
|
||||
|
||||
return {
|
||||
token,
|
||||
expiresIn: config.auth.sessionTimeout,
|
||||
role: sessionData.role,
|
||||
};
|
||||
}
|
||||
|
||||
getCookieOptions() {
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure: config.app.env === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: config.auth.sessionTimeout * 1000, // Convert to milliseconds
|
||||
path: '/',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthService();
|
||||
@@ -0,0 +1,414 @@
|
||||
const { exec, spawn } = require('child_process');
|
||||
const util = require('util');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const config = require('../config');
|
||||
const { logger } = require('../utils/logger');
|
||||
const { AppError } = require('../middleware/errorHandler.middleware');
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
class MinIOService {
|
||||
constructor(alias = config.minio.defaultAlias) {
|
||||
this.alias = alias;
|
||||
this.tempDir = path.join(__dirname, '../../../temp');
|
||||
this.ensureTempDir();
|
||||
}
|
||||
|
||||
async ensureTempDir() {
|
||||
try {
|
||||
await fs.mkdir(this.tempDir, { recursive: true });
|
||||
} catch (error) {
|
||||
logger.error('Failed to create temp directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute MinIO CLI command with timeout and error handling
|
||||
async executeCommand(command, options = {}) {
|
||||
const { timeout = 30000, parseJson = false } = options;
|
||||
|
||||
try {
|
||||
logger.debug(`Executing command: ${command}`);
|
||||
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
timeout,
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||||
env: { ...process.env, MC_NO_COLOR: '1' }, // Disable color output
|
||||
});
|
||||
|
||||
if (stderr && !stderr.includes('Configuration written to')) {
|
||||
logger.warn(`Command stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
if (parseJson && stdout) {
|
||||
// Handle multiple JSON objects (one per line)
|
||||
const lines = stdout.trim().split('\n').filter(line => line);
|
||||
if (lines.length === 1) {
|
||||
return JSON.parse(lines[0]);
|
||||
}
|
||||
return lines.map(line => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return stdout.trim();
|
||||
} catch (error) {
|
||||
logger.error(`Command failed: ${command}`, error);
|
||||
|
||||
if (error.code === 'ETIMEDOUT') {
|
||||
throw new AppError('Command timed out', 504);
|
||||
}
|
||||
|
||||
if (error.stderr) {
|
||||
if (error.stderr.includes('Unable to initialize new alias')) {
|
||||
throw new AppError('Invalid MinIO connection settings', 400);
|
||||
}
|
||||
if (error.stderr.includes('The specified bucket does not exist')) {
|
||||
throw new AppError('Bucket not found', 404);
|
||||
}
|
||||
if (error.stderr.includes('The specified key does not exist')) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
if (error.stderr.includes('Access Denied')) {
|
||||
throw new AppError('Access denied', 403);
|
||||
}
|
||||
}
|
||||
|
||||
throw new AppError(error.message || 'Command execution failed', 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Bucket Management Methods
|
||||
async listBuckets() {
|
||||
const output = await this.executeCommand(
|
||||
`mc ls ${this.alias} --json`,
|
||||
{ parseJson: true }
|
||||
);
|
||||
|
||||
return Array.isArray(output) ? output : [output];
|
||||
}
|
||||
|
||||
async createBucket(bucketName) {
|
||||
// Validate bucket name
|
||||
if (!this.isValidBucketName(bucketName)) {
|
||||
throw new AppError('Invalid bucket name. Must be 3-63 characters, lowercase, no spaces.', 400);
|
||||
}
|
||||
|
||||
await this.executeCommand(`mc mb ${this.alias}/${bucketName}`);
|
||||
return { message: 'Bucket created successfully', bucketName };
|
||||
}
|
||||
|
||||
async deleteBucket(bucketName) {
|
||||
// Check if bucket is empty first
|
||||
const objects = await this.executeCommand(
|
||||
`mc ls ${this.alias}/${bucketName} --json`,
|
||||
{ parseJson: true }
|
||||
);
|
||||
|
||||
if (objects && objects.length > 0) {
|
||||
throw new AppError('Cannot delete non-empty bucket', 400);
|
||||
}
|
||||
|
||||
await this.executeCommand(`mc rb ${this.alias}/${bucketName}`);
|
||||
return { message: 'Bucket deleted successfully', bucketName };
|
||||
}
|
||||
|
||||
async getBucketSize(bucketName) {
|
||||
const output = await this.executeCommand(
|
||||
`mc du --json ${this.alias}/${bucketName}`,
|
||||
{ parseJson: true }
|
||||
);
|
||||
|
||||
return {
|
||||
bucketName,
|
||||
size: output.size || 0,
|
||||
sizeFormatted: this.formatBytes(output.size || 0),
|
||||
objects: output.objects || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async getBucketSizes() {
|
||||
const buckets = await this.listBuckets();
|
||||
|
||||
const bucketSizes = await Promise.all(
|
||||
buckets.map(async (bucket) => {
|
||||
try {
|
||||
const sizeInfo = await this.getBucketSize(bucket.key);
|
||||
|
||||
// Get last modified time
|
||||
const findOutput = await this.executeCommand(
|
||||
`mc find ${this.alias}/${bucket.key} --maxdepth 1 --json | head -1`,
|
||||
{ parseJson: true }
|
||||
);
|
||||
|
||||
return {
|
||||
name: bucket.key,
|
||||
size: sizeInfo.size,
|
||||
sizeFormatted: sizeInfo.sizeFormatted,
|
||||
objects: sizeInfo.objects,
|
||||
lastModified: findOutput?.lastModified || bucket.lastModified || 'No files',
|
||||
created: bucket.lastModified,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get size for bucket ${bucket.key}:`, error);
|
||||
return {
|
||||
name: bucket.key,
|
||||
size: 0,
|
||||
sizeFormatted: '0 B',
|
||||
objects: 0,
|
||||
lastModified: 'Error',
|
||||
created: bucket.lastModified,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return bucketSizes;
|
||||
}
|
||||
|
||||
// User Management Methods
|
||||
async createBucketWithUser(bucketName, username, password) {
|
||||
// Validate inputs
|
||||
if (!this.isValidBucketName(bucketName)) {
|
||||
throw new AppError('Invalid bucket name', 400);
|
||||
}
|
||||
if (!this.isValidUsername(username)) {
|
||||
throw new AppError('Invalid username. Use only letters, numbers, hyphens, and underscores.', 400);
|
||||
}
|
||||
if (!this.isValidPassword(password)) {
|
||||
throw new AppError('Password must be at least 8 characters with uppercase, lowercase, and number.', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// Create bucket
|
||||
await this.createBucket(bucketName);
|
||||
|
||||
// Create user
|
||||
await this.executeCommand(
|
||||
`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}/*`
|
||||
]
|
||||
}]
|
||||
};
|
||||
|
||||
// Write policy to temp file
|
||||
const policyFile = path.join(this.tempDir, `${policyName}-${Date.now()}.json`);
|
||||
await fs.writeFile(policyFile, JSON.stringify(policy, null, 2));
|
||||
|
||||
try {
|
||||
// Create and attach policy
|
||||
await this.executeCommand(
|
||||
`mc admin policy create ${this.alias} ${policyName} ${policyFile}`
|
||||
);
|
||||
await this.executeCommand(
|
||||
`mc admin policy attach ${this.alias} ${policyName} --user ${username}`
|
||||
);
|
||||
} finally {
|
||||
// Clean up temp file
|
||||
await fs.unlink(policyFile).catch(() => {});
|
||||
}
|
||||
|
||||
return {
|
||||
bucketName,
|
||||
username,
|
||||
policyName,
|
||||
message: 'Bucket and user created successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
// Rollback on failure
|
||||
logger.error('Failed to create bucket with user, attempting rollback:', error);
|
||||
|
||||
// Try to clean up
|
||||
await this.executeCommand(`mc rb ${this.alias}/${bucketName} --force`).catch(() => {});
|
||||
await this.executeCommand(`mc admin user remove ${this.alias} ${username}`).catch(() => {});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async listUsers() {
|
||||
const output = await this.executeCommand(
|
||||
`mc admin user list ${this.alias} --json`,
|
||||
{ parseJson: true }
|
||||
);
|
||||
|
||||
const users = Array.isArray(output) ? output : [output];
|
||||
return users.map(user => ({
|
||||
accessKey: user.accessKey,
|
||||
status: user.userStatus,
|
||||
}));
|
||||
}
|
||||
|
||||
async removeUser(username) {
|
||||
await this.executeCommand(`mc admin user remove ${this.alias} ${username}`);
|
||||
return { message: 'User removed successfully', username };
|
||||
}
|
||||
|
||||
async enableUser(username) {
|
||||
await this.executeCommand(`mc admin user enable ${this.alias} ${username}`);
|
||||
return { message: 'User enabled successfully', username };
|
||||
}
|
||||
|
||||
async disableUser(username) {
|
||||
await this.executeCommand(`mc admin user disable ${this.alias} ${username}`);
|
||||
return { message: 'User disabled successfully', username };
|
||||
}
|
||||
|
||||
// Policy Management Methods
|
||||
async listPolicies() {
|
||||
const output = await this.executeCommand(
|
||||
`mc admin policy list ${this.alias} --json`,
|
||||
{ parseJson: true }
|
||||
);
|
||||
|
||||
const policies = Array.isArray(output) ? output : [output];
|
||||
return policies.map(policy => ({
|
||||
name: policy.policy,
|
||||
type: policy.policyInfo?.PolicyType || 'custom',
|
||||
}));
|
||||
}
|
||||
|
||||
async createPolicy(policyName, policyDocument) {
|
||||
if (!this.isValidPolicyName(policyName)) {
|
||||
throw new AppError('Invalid policy name. Use only letters, numbers, hyphens, and underscores.', 400);
|
||||
}
|
||||
|
||||
// Validate policy document
|
||||
try {
|
||||
const policy = typeof policyDocument === 'string'
|
||||
? JSON.parse(policyDocument)
|
||||
: policyDocument;
|
||||
|
||||
if (!policy.Version || !policy.Statement) {
|
||||
throw new Error('Invalid policy structure');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new AppError('Invalid policy document', 400);
|
||||
}
|
||||
|
||||
const policyFile = path.join(this.tempDir, `${policyName}-${Date.now()}.json`);
|
||||
|
||||
try {
|
||||
await fs.writeFile(policyFile,
|
||||
typeof policyDocument === 'string' ? policyDocument : JSON.stringify(policyDocument, null, 2)
|
||||
);
|
||||
|
||||
await this.executeCommand(
|
||||
`mc admin policy create ${this.alias} ${policyName} ${policyFile}`
|
||||
);
|
||||
|
||||
return { message: 'Policy created successfully', policyName };
|
||||
} finally {
|
||||
await fs.unlink(policyFile).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async deletePolicy(policyName) {
|
||||
await this.executeCommand(`mc admin policy remove ${this.alias} ${policyName}`);
|
||||
return { message: 'Policy deleted successfully', policyName };
|
||||
}
|
||||
|
||||
async attachPolicy(policyName, username) {
|
||||
await this.executeCommand(
|
||||
`mc admin policy attach ${this.alias} ${policyName} --user ${username}`
|
||||
);
|
||||
return { message: 'Policy attached successfully', policyName, username };
|
||||
}
|
||||
|
||||
// Alias Management Methods
|
||||
async testConnection(alias = this.alias) {
|
||||
try {
|
||||
await this.executeCommand(`mc admin info ${alias}`, { timeout: 10000 });
|
||||
return { status: 'connected', alias };
|
||||
} catch (error) {
|
||||
return { status: 'failed', alias, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async addAlias(aliasName, endpoint, accessKey, secretKey) {
|
||||
if (!this.isValidAliasName(aliasName)) {
|
||||
throw new AppError('Invalid alias name', 400);
|
||||
}
|
||||
|
||||
await this.executeCommand(
|
||||
`mc alias set ${aliasName} ${endpoint} ${accessKey} ${secretKey}`
|
||||
);
|
||||
|
||||
// Test the connection
|
||||
const testResult = await this.testConnection(aliasName);
|
||||
if (testResult.status !== 'connected') {
|
||||
// Remove the alias if connection fails
|
||||
await this.executeCommand(`mc alias remove ${aliasName}`).catch(() => {});
|
||||
throw new AppError('Failed to connect to MinIO server', 400);
|
||||
}
|
||||
|
||||
return { message: 'Alias added successfully', aliasName };
|
||||
}
|
||||
|
||||
async listAliases() {
|
||||
const output = await this.executeCommand(`mc alias list --json`, { parseJson: true });
|
||||
const aliases = Array.isArray(output) ? output : [output];
|
||||
|
||||
return aliases.map(alias => ({
|
||||
alias: alias.alias,
|
||||
URL: alias.URL,
|
||||
accessKey: alias.accessKey ? alias.accessKey.substring(0, 8) + '...' : '',
|
||||
}));
|
||||
}
|
||||
|
||||
// Validation Methods
|
||||
isValidBucketName(name) {
|
||||
const regex = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/;
|
||||
return name && name.length >= 3 && name.length <= 63 && regex.test(name);
|
||||
}
|
||||
|
||||
isValidUsername(username) {
|
||||
const regex = /^[a-zA-Z0-9_-]+$/;
|
||||
return username && username.length >= 3 && username.length <= 32 && regex.test(username);
|
||||
}
|
||||
|
||||
isValidPassword(password) {
|
||||
const hasUpperCase = /[A-Z]/.test(password);
|
||||
const hasLowerCase = /[a-z]/.test(password);
|
||||
const hasNumber = /\d/.test(password);
|
||||
return password && password.length >= 8 && hasUpperCase && hasLowerCase && hasNumber;
|
||||
}
|
||||
|
||||
isValidPolicyName(name) {
|
||||
const regex = /^[a-zA-Z0-9_-]+$/;
|
||||
return name && name.length >= 1 && name.length <= 128 && regex.test(name);
|
||||
}
|
||||
|
||||
isValidAliasName(name) {
|
||||
const regex = /^[a-zA-Z0-9_-]+$/;
|
||||
return name && name.length >= 1 && name.length <= 32 && regex.test(name);
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MinIOService;
|
||||
@@ -0,0 +1,233 @@
|
||||
const cron = require('node-cron');
|
||||
const nodemailer = require('nodemailer');
|
||||
const MinIOService = require('./minio.service');
|
||||
const config = require('../config');
|
||||
const { logger } = require('../utils/logger');
|
||||
|
||||
class ReportService {
|
||||
constructor() {
|
||||
this.minioService = new MinIOService();
|
||||
this.transporter = null;
|
||||
this.scheduledTask = null;
|
||||
|
||||
if (config.email.host && config.email.auth.user) {
|
||||
this.initializeMailer();
|
||||
this.setupSchedule();
|
||||
} else {
|
||||
logger.warn('Email configuration missing. Report scheduling disabled.');
|
||||
}
|
||||
}
|
||||
|
||||
initializeMailer() {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: config.email.host,
|
||||
port: config.email.port,
|
||||
secure: config.email.secure,
|
||||
auth: {
|
||||
user: config.email.auth.user,
|
||||
pass: config.email.auth.pass,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify connection
|
||||
this.transporter.verify((error) => {
|
||||
if (error) {
|
||||
logger.error('Email transporter verification failed:', error);
|
||||
} else {
|
||||
logger.info('Email transporter ready');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupSchedule() {
|
||||
if (!cron.validate(config.reports.schedule)) {
|
||||
logger.error(`Invalid cron expression: ${config.reports.schedule}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.scheduledTask = cron.schedule(config.reports.schedule, async () => {
|
||||
logger.info('Running scheduled storage report...');
|
||||
try {
|
||||
await this.generateAndSendReport();
|
||||
} catch (error) {
|
||||
logger.error('Scheduled report failed:', error);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Report scheduled with cron: ${config.reports.schedule}`);
|
||||
}
|
||||
|
||||
async generateReport() {
|
||||
const [bucketSizes, users] = await Promise.all([
|
||||
this.minioService.getBucketSizes(),
|
||||
this.minioService.listUsers(),
|
||||
]);
|
||||
|
||||
const totalSize = bucketSizes.reduce((sum, b) => sum + b.size, 0);
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
|
||||
const report = {
|
||||
date,
|
||||
summary: {
|
||||
totalBuckets: bucketSizes.length,
|
||||
totalUsers: users.length,
|
||||
totalSize,
|
||||
totalSizeFormatted: this.minioService.formatBytes(totalSize),
|
||||
},
|
||||
buckets: bucketSizes,
|
||||
users: users.map(u => u.accessKey),
|
||||
};
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
formatReportText(report) {
|
||||
let text = `MinIO Speicherauswertung\n`;
|
||||
text += `Datum: ${report.date}\n`;
|
||||
text += `----------------------------------------\n\n`;
|
||||
|
||||
text += `Zusammenfassung:\n`;
|
||||
text += `- Buckets: ${report.summary.totalBuckets}\n`;
|
||||
text += `- Benutzer: ${report.summary.totalUsers}\n`;
|
||||
text += `- Gesamtspeicher: ${report.summary.totalSizeFormatted}\n\n`;
|
||||
|
||||
text += `Alle MinIO-User:\n`;
|
||||
report.users.forEach(user => {
|
||||
text += `- ${user}\n`;
|
||||
});
|
||||
|
||||
text += `\nAlle Buckets:\n`;
|
||||
report.buckets.forEach(bucket => {
|
||||
text += `\nBucket: ${bucket.name}\n`;
|
||||
text += ` Größe: ${bucket.sizeFormatted}\n`;
|
||||
text += ` Objekte: ${bucket.objects}\n`;
|
||||
text += ` Letzte Änderung: ${bucket.lastModified}\n`;
|
||||
});
|
||||
|
||||
text += `\n----------------------------------------\n`;
|
||||
text += `Gesamtspeicher: ${report.summary.totalSizeFormatted}\n`;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
formatReportHTML(report) {
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
h1 { color: #2c3e50; }
|
||||
.summary { background: #f4f4f4; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #2c3e50; color: white; }
|
||||
tr:nth-child(even) { background-color: #f2f2f2; }
|
||||
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 0.9em; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MinIO Speicherauswertung</h1>
|
||||
<p>Datum: ${report.date}</p>
|
||||
|
||||
<div class="summary">
|
||||
<h2>Zusammenfassung</h2>
|
||||
<ul>
|
||||
<li>Anzahl Buckets: ${report.summary.totalBuckets}</li>
|
||||
<li>Anzahl Benutzer: ${report.summary.totalUsers}</li>
|
||||
<li>Gesamtspeicher: <strong>${report.summary.totalSizeFormatted}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Bucket-Details</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bucket Name</th>
|
||||
<th>Größe</th>
|
||||
<th>Objekte</th>
|
||||
<th>Letzte Änderung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${report.buckets.map(bucket => `
|
||||
<tr>
|
||||
<td>${bucket.name}</td>
|
||||
<td>${bucket.sizeFormatted}</td>
|
||||
<td>${bucket.objects}</td>
|
||||
<td>${bucket.lastModified}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Benutzer</h2>
|
||||
<ul>
|
||||
${report.users.map(user => `<li>${user}</li>`).join('')}
|
||||
</ul>
|
||||
|
||||
<div class="footer">
|
||||
<p>Dieser Bericht wurde automatisch von MinIO WebUI generiert.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async sendEmail(report, recipients = null) {
|
||||
if (!this.transporter) {
|
||||
throw new Error('Email service not configured');
|
||||
}
|
||||
|
||||
const to = recipients || config.email.to;
|
||||
if (!to) {
|
||||
throw new Error('No recipients configured');
|
||||
}
|
||||
|
||||
const mailOptions = {
|
||||
from: config.email.from,
|
||||
to: Array.isArray(to) ? to.join(', ') : to,
|
||||
subject: `MinIO Speicherauswertung ${report.date}`,
|
||||
text: this.formatReportText(report),
|
||||
html: this.formatReportHTML(report),
|
||||
};
|
||||
|
||||
const info = await this.transporter.sendMail(mailOptions);
|
||||
logger.info(`Report email sent: ${info.messageId}`);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
async generateAndSendReport(recipients = null) {
|
||||
const report = await this.generateReport();
|
||||
await this.sendEmail(report, recipients);
|
||||
return report;
|
||||
}
|
||||
|
||||
getScheduleInfo() {
|
||||
return {
|
||||
enabled: !!this.scheduledTask,
|
||||
schedule: config.reports.schedule,
|
||||
nextRun: this.scheduledTask ? cron.getTasks()[0]?.nextDates(1)[0] : null,
|
||||
recipients: config.email.to,
|
||||
};
|
||||
}
|
||||
|
||||
stopSchedule() {
|
||||
if (this.scheduledTask) {
|
||||
this.scheduledTask.stop();
|
||||
this.scheduledTask = null;
|
||||
logger.info('Report schedule stopped');
|
||||
}
|
||||
}
|
||||
|
||||
startSchedule() {
|
||||
if (!this.scheduledTask && this.transporter) {
|
||||
this.setupSchedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ReportService();
|
||||
Reference in New Issue
Block a user