Compare commits
2 Commits
fc2c1cf927
...
c53c0f8814
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c53c0f8814 | ||
|
|
8cb43b05a8 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,4 +4,5 @@ config
|
|||||||
logs
|
logs
|
||||||
api/.env
|
api/.env
|
||||||
api/node_modules
|
api/node_modules
|
||||||
certs
|
certs
|
||||||
|
backups
|
||||||
@ -103,13 +103,14 @@ app.use((err, req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cron = require('node-cron');
|
const cron = require('node-cron');
|
||||||
const { cleanupOrphanedMailboxes, cleanupUnmatchedAndExpired, cleanupInactiveMailboxes } = require('./scripts/cleanup');
|
const { cleanupOrphanedMailboxes, cleanupUnmatchedAndExpired, cleanupInactiveMailboxes, cleanupInactiveUsers } = require('./scripts/cleanup');
|
||||||
cron.schedule('0 0 * * *', async () => {
|
cron.schedule('0 0 * * *', async () => {
|
||||||
console.log('Running mailbox cleanup job');
|
console.log('Running mailbox cleanup job');
|
||||||
try {
|
try {
|
||||||
await cleanupOrphanedMailboxes();
|
await cleanupOrphanedMailboxes();
|
||||||
await cleanupUnmatchedAndExpired();
|
await cleanupUnmatchedAndExpired();
|
||||||
await cleanupInactiveMailboxes();
|
await cleanupInactiveMailboxes();
|
||||||
|
await cleanupInactiveUsers();
|
||||||
console.log('Mailbox cleanup completed successfully');
|
console.log('Mailbox cleanup completed successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Mailbox cleanup failed:', error);
|
console.error('Mailbox cleanup failed:', error);
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
require('dotenv').config();
|
||||||
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
|
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
|
||||||
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
|
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { format, differenceInHours, addHours } = require('date-fns');
|
const { format, differenceInHours, addHours } = require('date-fns');
|
||||||
|
const sendMail = require('../utils/sendMail.js');
|
||||||
|
const HaveIBeenPwnedAPI = require('../utils/hibp.js');
|
||||||
|
|
||||||
class AuthController extends BaseController {
|
class AuthController extends BaseController {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.protected('me', 'refreshToken', 'login');
|
this.protected('me', 'refreshToken', 'login');
|
||||||
this.admin('register');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,8 +62,16 @@ class AuthController extends BaseController {
|
|||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
}
|
}
|
||||||
const token = jwt.sign({ id: user.id, username: user.username, is_admin: user.is_admin, email: user.email }, process.env.JWT_SECRET, { expiresIn: '6h' });
|
|
||||||
res.json({ api_key: token });
|
const validToken = jwt.verify(user.api_key, process.env.JWT_SECRET);
|
||||||
|
if (!validToken) {
|
||||||
|
const token = jwt.sign({ id: user.id, username: user.username, is_admin: user.is_admin, email: user.email }, process.env.JWT_SECRET, { expiresIn: '6h' });
|
||||||
|
await models.User.query().update({ api_key: token, last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss') }).where('id', user.id);
|
||||||
|
return res.json({ api_key: token });
|
||||||
|
}
|
||||||
|
|
||||||
|
await models.User.query().update({ last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss') }).where('id', user.id);
|
||||||
|
res.json({ api_key: user.api_key });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,29 +169,66 @@ class AuthController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async register(req, res) {
|
async register(req, res) {
|
||||||
|
console.log(req.body);
|
||||||
|
|
||||||
const { username, email, password } = req.body;
|
const { username, email, password } = req.body;
|
||||||
|
|
||||||
|
const hibp = new HaveIBeenPwnedAPI();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const checkBreached = await hibp.checkPassword(password);
|
||||||
|
if (checkBreached.isCompromised) {
|
||||||
|
return res.status(400).json({ error: 'Password is compromised. Checked against haveibeenpwned.com' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatePassword = await hibp.validatePassword(password, { maxExposures: 0 });
|
||||||
|
if (!validatePassword.isValid) {
|
||||||
|
return res.status(400).json({ error: 'Password is not valid. Checked against haveibeenpwned.com' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({ error: 'Failed to check password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dupUser = await models.User.query().where('username', username).orWhere('email', email).first();
|
||||||
|
if (dupUser) {
|
||||||
|
return res.status(400).json({ error: 'Username or email already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
const invite_token = crypto.randomBytes(32).toString('hex');
|
const invite_token = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
const salt = await bcrypt.genSalt(10);
|
const salt = await bcrypt.genSalt(10);
|
||||||
const passwordEncrypted = await bcrypt.hash(password, salt);
|
const passwordEncrypted = await bcrypt.hash(password, salt);
|
||||||
|
const newEmail = `${username}@${crypto.randomBytes(10).toString('hex')}.com`;
|
||||||
const user = await models.User.query().insert({
|
const user = await models.User.query().insert({
|
||||||
username,
|
username,
|
||||||
password: passwordEncrypted,
|
password: passwordEncrypted,
|
||||||
email,
|
email: newEmail,
|
||||||
is_admin: 0,
|
is_admin: 0,
|
||||||
is_active: 0,
|
is_active: 0,
|
||||||
created: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
created: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss')
|
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
|
last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss')
|
||||||
});
|
});
|
||||||
|
|
||||||
await models.Invite.query().insert({
|
await models.Invite.query().insert({
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
token: invite_token,
|
token: invite_token,
|
||||||
expires_at: format(addHours(new Date(), 12), 'yyyy-MM-dd HH:mm:ss')
|
expires: format(addHours(new Date(), 12), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
|
created: format(new Date(), 'yyyy-MM-dd HH:mm:ss')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await sendMail(
|
||||||
|
email,
|
||||||
|
'2weekmail - Invite Code',
|
||||||
|
`Welcome to the 2weekmail. Please use the link below to activate your account.
|
||||||
|
<br>
|
||||||
|
<a href="https://2weekmail.fyi/auth/activate/${invite_token}">activate your account</a>`,
|
||||||
|
`Welcome to the 2weekmail. Please use it to <a href="https://2weekmail.fyi/auth/activate/${invite_token}">activate your account</a>.`
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: 'User registered successfully',
|
message: 'User registered successfully'
|
||||||
invite_token
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +237,7 @@ class AuthController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async activate(req, res) {
|
async activate(req, res) {
|
||||||
const { invite_token } = req.body;
|
const invite_token = req.params.token;
|
||||||
const invite = await models.Invite.query().where('token', invite_token).first();
|
const invite = await models.Invite.query().where('token', invite_token).first();
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
return res.status(400).json({ error: 'Invalid invite token' });
|
return res.status(400).json({ error: 'Invalid invite token' });
|
||||||
@ -207,12 +254,15 @@ class AuthController extends BaseController {
|
|||||||
|
|
||||||
await models.User.query().update({
|
await models.User.query().update({
|
||||||
is_active: 1,
|
is_active: 1,
|
||||||
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss')
|
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
|
last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss')
|
||||||
}).where('id', user.id);
|
}).where('id', user.id);
|
||||||
|
|
||||||
res.json({
|
await models.Invite.query().delete().where('id', invite.id);
|
||||||
message: 'User activated successfully',
|
|
||||||
invite_token
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'User activated successfully'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,7 +94,6 @@ class MailboxController extends BaseController {
|
|||||||
const mailData = {
|
const mailData = {
|
||||||
id: mailbox.id,
|
id: mailbox.id,
|
||||||
username: mailbox.username,
|
username: mailbox.username,
|
||||||
password: password.password,
|
|
||||||
expires: mailbox.expires
|
expires: mailbox.expires
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
api/db/migrations/20250323115335_add_ignore_to_users.js
Normal file
20
api/db/migrations/20250323115335_add_ignore_to_users.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.alterTable('users', (table) => {
|
||||||
|
table.tinyint('ignore').defaultTo(0);
|
||||||
|
table.datetime('last_login').nullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.alterTable('users', (table) => {
|
||||||
|
table.dropColumn('ignore');
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -13,14 +13,14 @@ class Invite extends BaseModel {
|
|||||||
static get jsonSchema() {
|
static get jsonSchema() {
|
||||||
return {
|
return {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['user_id', 'token', 'expires_at'],
|
required: ['user_id', 'token', 'expires'],
|
||||||
|
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'integer' },
|
id: { type: 'integer' },
|
||||||
user_id: { type: 'string', minLength: 1, maxLength: 255 },
|
user_id: { type: 'integer' },
|
||||||
token: { type: 'string', minLength: 1 },
|
token: { type: 'string', minLength: 1 },
|
||||||
expires_at: { type: 'string', format: 'date-time' },
|
expires: { type: 'string', format: 'date-time' },
|
||||||
created_at: { type: 'string', format: 'date-time' },
|
created: { type: 'string', format: 'date-time' },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,17 +12,17 @@ class User extends BaseModel {
|
|||||||
static get jsonSchema() {
|
static get jsonSchema() {
|
||||||
return {
|
return {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['username', 'password', 'email'],
|
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'integer' },
|
id: { type: 'integer' },
|
||||||
username: { type: 'string', minLength: 1, maxLength: 255 },
|
username: { type: 'string', minLength: 1, maxLength: 255 },
|
||||||
password: { type: 'string', minLength: 1 },
|
password: { type: 'string', minLength: 1 },
|
||||||
email: { type: 'string', minLength: 1, maxLength: 255 },
|
email: { type: 'string', minLength: 1, maxLength: 255 },
|
||||||
is_admin: { type: 'boolean', default: false },
|
is_admin: { type: 'integer', default: 0 },
|
||||||
is_active: { type: 'boolean', default: true },
|
is_active: { type: 'integer', default: 0 },
|
||||||
api_key: { type: ['string', 'null'] },
|
api_key: { type: ['string', 'null'] },
|
||||||
created: { type: 'string', format: 'date-time' },
|
created: { type: 'string', format: 'date-time' },
|
||||||
modified: { type: 'string', format: 'date-time' }
|
modified: { type: 'string', format: 'date-time' },
|
||||||
|
ignore: { type: 'integer', default: 0 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,10 @@ const { format } = require("date-fns");
|
|||||||
* @returns { Promise<void> }
|
* @returns { Promise<void> }
|
||||||
*/
|
*/
|
||||||
exports.seed = async function(knex) {
|
exports.seed = async function(knex) {
|
||||||
// Deletes ALL existing entries
|
const user = await knex('users').where('username', 'admin').first();
|
||||||
await knex('users').del()
|
if (user) {
|
||||||
|
await knex('users').where('id', user.id).del();
|
||||||
|
}
|
||||||
await knex('users').insert([
|
await knex('users').insert([
|
||||||
{
|
{
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
|
|||||||
1
api/public/css/sweetalert2.min.css
vendored
Normal file
1
api/public/css/sweetalert2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
api/public/img/favicon.ico
Normal file
BIN
api/public/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
api/public/img/favicon.png
Normal file
BIN
api/public/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
6
api/public/js/sweetalert2.js
Normal file
6
api/public/js/sweetalert2.js
Normal file
File diff suppressed because one or more lines are too long
@ -3,8 +3,8 @@ const AuthController = require(path.resolve(process.env.ROOT_PATH, './controller
|
|||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
|
|
||||||
router.post('/register', AuthController.register);
|
router.post('/register', AuthController.register);
|
||||||
router.post('/activate', AuthController.activate);
|
router.get('/activate/:token', AuthController.activateView);
|
||||||
|
router.post('/activate/:token', AuthController.activate);
|
||||||
router.get('/register', AuthController.registerView);
|
router.get('/register', AuthController.registerView);
|
||||||
router.get('/activate', AuthController.activateView);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { models } = require('../db/db');
|
const { models } = require('../db/db');
|
||||||
const { Mailbox, Alias } = models;
|
const { Mailbox, Alias, User } = models;
|
||||||
const { format, isBefore } = require('date-fns');
|
const { format, isBefore, subDays } = require('date-fns');
|
||||||
|
|
||||||
async function scanMailboxDirectories(callback) {
|
async function scanMailboxDirectories(callback) {
|
||||||
const mailPath = '/var/mail';
|
const mailPath = '/var/mail';
|
||||||
@ -192,9 +192,31 @@ async function cleanupInactiveMailboxes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function cleanupInactiveUsers() {
|
||||||
|
try {
|
||||||
|
const cutoffDate = format(subDays(new Date(), 30), 'yyyy-MM-dd HH:mm:ss');
|
||||||
|
|
||||||
|
const result = await User.query()
|
||||||
|
.delete()
|
||||||
|
.where('ignore', 0)
|
||||||
|
.where('last_login', '<', cutoffDate)
|
||||||
|
.returning('username');
|
||||||
|
|
||||||
|
const removed = result.length;
|
||||||
|
|
||||||
|
result.forEach(user => {
|
||||||
|
console.log(`Removing inactive user: ${user.username}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Cleanup of inactive users completed - removed ${removed} users`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during cleanup:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
cleanupOrphanedMailboxes,
|
cleanupOrphanedMailboxes,
|
||||||
cleanupUnmatchedAndExpired,
|
cleanupUnmatchedAndExpired,
|
||||||
cleanupInactiveMailboxes
|
cleanupInactiveMailboxes,
|
||||||
|
cleanupInactiveUsers
|
||||||
};
|
};
|
||||||
64
api/test.js
64
api/test.js
@ -1,6 +1,60 @@
|
|||||||
const { models } = require('../db/db.js');
|
const nodemailer = require('nodemailer');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
(async () => {
|
// Create a transport configuration for Postfix
|
||||||
const domains = await models.Domain.query();
|
const transport = nodemailer.createTransport({
|
||||||
console.log(domains);
|
host: '172.18.0.3', // Use the Docker service name for Postfix
|
||||||
})();
|
// or host: 'mail', // depending on your docker-compose service name
|
||||||
|
port: 587, // Default SMTP port
|
||||||
|
secure: false, // TLS is not required for local Postfix
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER, // Add your SMTP username
|
||||||
|
pass: process.env.SMTP_PASS // Add your SMTP password
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false // Allow self-signed certificates
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an email using the configured Postfix transport
|
||||||
|
* @param {string} to - Recipient email address
|
||||||
|
* @param {string} subject - Email subject
|
||||||
|
* @param {string} text - Plain text email content
|
||||||
|
* @param {string} html - HTML email content (optional)
|
||||||
|
* @returns {Promise} Result of sending the email
|
||||||
|
*/
|
||||||
|
async function sendEmail(to, subject, text, html) {
|
||||||
|
try {
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.SMTP_FROM, // Replace with your sender email
|
||||||
|
to: to,
|
||||||
|
subject: subject,
|
||||||
|
text: text,
|
||||||
|
html: html || text // Use HTML if provided, otherwise use plain text
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = await transport.sendMail(mailOptions);
|
||||||
|
console.log('Email sent successfully:', info.messageId);
|
||||||
|
return info;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending email:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
await sendEmail(
|
||||||
|
'ryancarr10@gmail.com',
|
||||||
|
'Test Email',
|
||||||
|
'This is a test email from Postfix',
|
||||||
|
'<h1>This is a test email from Postfix</h1>'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Main error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|||||||
0
api/utils/checkPassword.js
Normal file
0
api/utils/checkPassword.js
Normal file
92
api/utils/hibp.js
Normal file
92
api/utils/hibp.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
class HaveIBeenPwnedAPI {
|
||||||
|
constructor(config = {}) {
|
||||||
|
this.baseUrl = 'https://api.pwnedpasswords.com';
|
||||||
|
this.userAgent = config.userAgent || 'PasswordSecurityChecker';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a SHA-1 hash of the password
|
||||||
|
* @param {string} password - The password to hash
|
||||||
|
* @returns {string} The uppercase SHA-1 hash
|
||||||
|
*/
|
||||||
|
generateHash(password) {
|
||||||
|
return crypto
|
||||||
|
.createHash('sha1')
|
||||||
|
.update(password)
|
||||||
|
.digest('hex')
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a password has been exposed in known data breaches
|
||||||
|
* @param {string} password - The password to check
|
||||||
|
* @returns {Promise<{isCompromised: boolean, timesExposed: number}>}
|
||||||
|
*/
|
||||||
|
async checkPassword(password) {
|
||||||
|
try {
|
||||||
|
// Generate hash and get first 5 characters for k-anonymity
|
||||||
|
const hash = this.generateHash(password);
|
||||||
|
const hashPrefix = hash.substring(0, 5);
|
||||||
|
const hashSuffix = hash.substring(5);
|
||||||
|
|
||||||
|
// Make request to HIBP API
|
||||||
|
const response = await axios.get(`${this.baseUrl}/range/${hashPrefix}`, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': this.userAgent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse response and check if password hash suffix exists
|
||||||
|
const hashes = response.data.split('\n');
|
||||||
|
const match = hashes.find(h => h.split(':')[0] === hashSuffix);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const timesExposed = parseInt(match.split(':')[1]);
|
||||||
|
return {
|
||||||
|
isCompromised: true,
|
||||||
|
timesExposed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCompromised: false,
|
||||||
|
timesExposed: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to check password: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a password against HIBP and custom rules
|
||||||
|
* @param {string} password - The password to validate
|
||||||
|
* @param {Object} options - Validation options
|
||||||
|
* @param {number} options.maxExposures - Maximum allowed exposures (default: 0)
|
||||||
|
* @returns {Promise<{isValid: boolean, reason?: string}>}
|
||||||
|
*/
|
||||||
|
async validatePassword(password, options = { maxExposures: 0 }) {
|
||||||
|
try {
|
||||||
|
const result = await this.checkPassword(password);
|
||||||
|
|
||||||
|
if (result.timesExposed > options.maxExposures) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
reason: `Password has been exposed ${result.timesExposed} times in data breaches`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Password validation failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HaveIBeenPwnedAPI;
|
||||||
46
api/utils/sendMail.js
Normal file
46
api/utils/sendMail.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
// Create a transport configuration for Postfix
|
||||||
|
const transport = nodemailer.createTransport({
|
||||||
|
host: '172.18.0.3', // Use the Docker service name for Postfix
|
||||||
|
// or host: 'mail', // depending on your docker-compose service name
|
||||||
|
port: 587, // Default SMTP port
|
||||||
|
secure: false, // TLS is not required for local Postfix
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER, // Add your SMTP username
|
||||||
|
pass: process.env.SMTP_PASS // Add your SMTP password
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false // Allow self-signed certificates
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an email using the configured Postfix transport
|
||||||
|
* @param {string} to - Recipient email address
|
||||||
|
* @param {string} subject - Email subject
|
||||||
|
* @param {string} text - Plain text email content
|
||||||
|
* @param {string} html - HTML email content (optional)
|
||||||
|
* @returns {Promise} Result of sending the email
|
||||||
|
*/
|
||||||
|
async function sendMail(to, subject, text, html) {
|
||||||
|
try {
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.SMTP_FROM,
|
||||||
|
to: to,
|
||||||
|
subject: subject,
|
||||||
|
text: text,
|
||||||
|
html: html || text
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = await transport.sendMail(mailOptions);
|
||||||
|
console.log('Email sent successfully:', info.messageId);
|
||||||
|
return info;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending email:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = sendMail;
|
||||||
@ -176,15 +176,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-6 mb-3">
|
<div class="col-md-3 col-6 mb-3">
|
||||||
<i class="fas fa-database fa-3x text-primary mb-2"></i>
|
<i class="fas fa-database fa-3x text-primary mb-2"></i>
|
||||||
<p>MongoDB</p>
|
<p>MySQL (knex.js & objection.js)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-6 mb-3">
|
<div class="col-md-3 col-6 mb-3">
|
||||||
<i class="fas fa-lock fa-3x text-primary mb-2"></i>
|
<i class="fas fa-lock fa-3x text-primary mb-2"></i>
|
||||||
<p>Encryption</p>
|
<p>Encryption (bcrypt, jwt)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-6 mb-3">
|
<div class="col-md-3 col-6 mb-3">
|
||||||
<i class="fas fa-server fa-3x text-primary mb-2"></i>
|
<i class="fas fa-server fa-3x text-primary mb-2"></i>
|
||||||
<p>Cloud Hosting</p>
|
<p>Cloud Hosting (Cherry Servers)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<div id="success" class="d-none">
|
<div id="success" class="d-none">
|
||||||
<i class="fas fa-check-circle text-success fa-5x mb-3"></i>
|
<i class="fas fa-check-circle text-success fa-5x mb-3"></i>
|
||||||
<h4 class="mb-3">Account Successfully Activated!</h4>
|
<h4 class="mb-3">Account Successfully Activated!</h4>
|
||||||
<p>Your API key has been generated and sent to your email address.</p>
|
<p>Use your username and password to login on the API and get your API key.</p>
|
||||||
<div class="alert alert-info mt-4">
|
<div class="alert alert-info mt-4">
|
||||||
<p class="mb-0"><strong>Important:</strong> Keep your API key secure and do not share it with others.</p>
|
<p class="mb-0"><strong>Important:</strong> Keep your API key secure and do not share it with others.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -33,22 +33,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#section 'scripts'}}
|
||||||
<script>
|
<script>
|
||||||
// Simulate activation process
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Get activation token from URL
|
const token = window.location.pathname.split('/activate/')[1];
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const token = urlParams.get('token');
|
|
||||||
|
|
||||||
// This would be replaced with an actual API call
|
if (token) {
|
||||||
setTimeout(() => {
|
$.ajax({
|
||||||
document.getElementById('loading').classList.add('d-none');
|
url: `/auth/activate/${token}`,
|
||||||
|
method: 'POST',
|
||||||
if (token) {
|
success: function(response) {
|
||||||
document.getElementById('success').classList.remove('d-none');
|
$('#loading').addClass('d-none');
|
||||||
} else {
|
$('#success').removeClass('d-none');
|
||||||
document.getElementById('error').classList.remove('d-none');
|
},
|
||||||
}
|
error: function(xhr, status, error) {
|
||||||
}, 2000);
|
console.error('Activation error:', error);
|
||||||
|
$('#loading').addClass('d-none');
|
||||||
|
$('#error').removeClass('d-none');
|
||||||
|
|
||||||
|
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||||||
|
$('#error p').text(xhr.responseJSON.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$('#loading').addClass('d-none');
|
||||||
|
$('#error').removeClass('d-none');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
{{/section}}
|
||||||
@ -5,27 +5,29 @@
|
|||||||
<h3 class="mb-0">Register for API Access</h3>
|
<h3 class="mb-0">Register for API Access</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="registerForm" action="/auth/register" method="POST">
|
<form id="registerForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email address</label>
|
<label for="email" class="form-label">Email address</label>
|
||||||
<input type="email" class="form-control" id="email" name="email" required>
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
<div class="form-text">We'll send a verification link to this email.</div>
|
<div class="form-text">We'll send a verification link to this email.
|
||||||
|
<p>You're email will be deleted after verification.<br>
|
||||||
|
<b>Remember your password! No resets will be allowed.</b></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Full Name</label>
|
<label for="username" class="form-label">Username</label>
|
||||||
<input type="text" class="form-control" id="name" name="name" required>
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="organization" class="form-label">Organization (Optional)</label>
|
<label for="password" class="form-label">Password</label>
|
||||||
<input type="text" class="form-control" id="organization" name="organization">
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="usage" class="form-label">Intended Usage</label>
|
<label for="confirm_password" class="form-label">Confirm Password</label>
|
||||||
<textarea class="form-control" id="usage" name="usage" rows="3" required></textarea>
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||||
<div class="form-text">Briefly describe how you plan to use our API.</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
@ -33,9 +35,65 @@
|
|||||||
<label class="form-check-label" for="terms">I agree to the <a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a></label>
|
<label class="form-check-label" for="terms">I agree to the <a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Register</button>
|
<button type="submit" class="btn btn-primary" id="registerButton">Register</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{#section 'scripts'}}
|
||||||
|
<script>
|
||||||
|
$('#registerButton').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const email = $('#email').val();
|
||||||
|
const username = $('#username').val();
|
||||||
|
const password = $('#password').val();
|
||||||
|
const confirm_password = $('#confirm_password').val();
|
||||||
|
const terms = $('#terms').is(':checked');
|
||||||
|
|
||||||
|
if (password !== confirm_password) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Passwords do not match',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!terms) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error',
|
||||||
|
text: 'You must agree to the Terms of Service and Privacy Policy',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(email, username, password);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/auth/register',
|
||||||
|
type: 'POST',
|
||||||
|
data: { email, username, password },
|
||||||
|
success: function(response) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Success',
|
||||||
|
text: `You have been registered successfully!<br>
|
||||||
|
Your invite code is only good for 12 hours.
|
||||||
|
<br>Please check your email for verification.
|
||||||
|
<br>Check your spam folder if you don't see it in your inbox.`,
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
const errorMessage = xhr.responseJSON?.error || 'An error occurred while registering. Please try again.';
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error',
|
||||||
|
text: errorMessage,
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{/section}}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<h1 class="display-4 fw-bold">2weekmail</h1>
|
<h1 class="display-4 fw-bold">2weekmail</h1>
|
||||||
<p class="lead">Temporary email service that lasts for two weeks</p>
|
<p class="lead">Temporary email service that lasts for two weeks</p>
|
||||||
<div class="d-flex justify-content-center mt-4">
|
<div class="d-flex justify-content-center mt-4">
|
||||||
<a href="/api-docs" class="btn btn-primary btn-lg me-3">
|
<a href="/auth/register" class="btn btn-primary btn-lg me-3">
|
||||||
<i class="fas fa-code me-2"></i>Get API Key
|
<i class="fas fa-code me-2"></i>Get API Key
|
||||||
</a>
|
</a>
|
||||||
<a href="#features" class="btn btn-outline-light btn-lg">
|
<a href="#features" class="btn btn-outline-light btn-lg">
|
||||||
|
|||||||
@ -16,5 +16,7 @@
|
|||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="/js/all.min.js"></script>
|
<script src="/js/all.min.js"></script>
|
||||||
<script src="/js/chart.min.js"></script>
|
<script src="/js/chart.min.js"></script>
|
||||||
|
<script src="/js/sweetalert2.js"></script>
|
||||||
|
{{{_sections.scripts}}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,11 +1,20 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="2weekmail is a temporary email service that allows you to send and receive emails for a limited time.">
|
||||||
|
<meta name="keywords" content="temporary email, disposable email, temporary email service, disposable email service, temporary email address, disposable email address, temporary email service, disposable email service, temporary email address, disposable email address">
|
||||||
|
<link rel="icon" href="/img/favicon.ico" type="image/x-icon">
|
||||||
|
<meta name="author" content="2weekmail">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<meta name="googlebot" content="index, follow">
|
||||||
|
<meta name="bingbot" content="index, follow">
|
||||||
|
<meta name="yandexbot" content="index, follow">
|
||||||
<title>2weekmail - Temporary Email Service</title>
|
<title>2weekmail - Temporary Email Service</title>
|
||||||
<!-- Bootstrap 5 CSS -->
|
<!-- Bootstrap 5 CSS -->
|
||||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<!-- Font Awesome for icons -->
|
<!-- Font Awesome for icons -->
|
||||||
<link rel="stylesheet" href="/css/all.min.css">
|
<link rel="stylesheet" href="/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/sweetalert2.min.css">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
@ -21,6 +21,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/about"><i class="fas fa-info-circle me-1"></i> About</a>
|
<a class="nav-link" href="/about"><i class="fas fa-info-circle me-1"></i> About</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/auth/register"><i class="fas fa-user-plus me-1"></i> Register</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -116,9 +116,9 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Fetch all stats data
|
// Fetch all stats data
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch('https://api.2weekmail.test/stats/system').then(res => res.json()),
|
fetch('https://api.2weekmail.fyi/stats/system').then(res => res.json()),
|
||||||
fetch('https://api.2weekmail.test/stats/mailboxes').then(res => res.json()),
|
fetch('https://api.2weekmail.fyi/stats/mailboxes').then(res => res.json()),
|
||||||
fetch('https://api.2weekmail.test/stats/domains').then(res => res.json())
|
fetch('https://api.2weekmail.fyi/stats/domains').then(res => res.json())
|
||||||
])
|
])
|
||||||
.then(([systemStats, mailboxStats, domainStats]) => {
|
.then(([systemStats, mailboxStats, domainStats]) => {
|
||||||
// Update overview cards
|
// Update overview cards
|
||||||
|
|||||||
@ -2,13 +2,25 @@ const express = require('express');
|
|||||||
const exphbs = require('express-handlebars');
|
const exphbs = require('express-handlebars');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const app = express();
|
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
const app = express();
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const PORT = process.env.WEB_PORT || 3350;
|
const PORT = process.env.WEB_PORT || 3350;
|
||||||
const IP = process.env.IP || '0.0.0.0';
|
const IP = process.env.IP || '0.0.0.0';
|
||||||
|
const webAuthRoutes = require('./routes/webAuth');
|
||||||
|
|
||||||
app.use(cors());
|
const corsOptions = {
|
||||||
|
origin: [
|
||||||
|
'https://2weekmail.fyi',
|
||||||
|
'http://localhost:3350'
|
||||||
|
],
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||||
|
credentials: true
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// Configure Helmet with custom CSP
|
// Configure Helmet with custom CSP
|
||||||
// app.use(helmet({
|
// app.use(helmet({
|
||||||
@ -32,7 +44,14 @@ app.engine('hbs', exphbs.engine({
|
|||||||
extname: '.hbs',
|
extname: '.hbs',
|
||||||
defaultLayout: 'main',
|
defaultLayout: 'main',
|
||||||
layoutsDir: path.join(__dirname, 'views/layouts'),
|
layoutsDir: path.join(__dirname, 'views/layouts'),
|
||||||
partialsDir: path.join(__dirname, 'views/partials')
|
partialsDir: path.join(__dirname, 'views/partials'),
|
||||||
|
helpers: {
|
||||||
|
section: function (name, options) {
|
||||||
|
if (!this._sections) this._sections = {};
|
||||||
|
this._sections[name] = options.fn(this);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
app.set('view engine', 'hbs');
|
app.set('view engine', 'hbs');
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
@ -46,6 +65,8 @@ app.get('/', (req, res) => {
|
|||||||
res.render('home');
|
res.render('home');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use('/auth', webAuthRoutes);
|
||||||
|
|
||||||
app.get('/stats', (req, res) => {
|
app.get('/stats', (req, res) => {
|
||||||
res.render('stats');
|
res.render('stats');
|
||||||
});
|
});
|
||||||
@ -54,15 +75,6 @@ app.get('/about', (req, res) => {
|
|||||||
res.render('about');
|
res.render('about');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auth routes (if needed)
|
|
||||||
app.get('/auth/register', (req, res) => {
|
|
||||||
res.render('auth/register');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/auth/activate', (req, res) => {
|
|
||||||
res.render('auth/activate');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/privacy', (req, res) => {
|
app.get('/privacy', (req, res) => {
|
||||||
res.render('policies/privacy');
|
res.render('policies/privacy');
|
||||||
});
|
});
|
||||||
|
|||||||
217
backup.sh
Executable file
217
backup.sh
Executable file
@ -0,0 +1,217 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
if [ -f .env ]; then
|
||||||
|
# Read each line from .env and export variables
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
# Skip comments and empty lines
|
||||||
|
[[ $line =~ ^#.*$ ]] && continue
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
|
||||||
|
# Export the variable
|
||||||
|
export "$line"
|
||||||
|
done < .env
|
||||||
|
else
|
||||||
|
echo "Error: .env file not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Variables (now using environment variables with defaults)
|
||||||
|
MYSQL_USER="admin"
|
||||||
|
MYSQL_PASSWORD=${MYSQL_ADMIN_PASSWORD}
|
||||||
|
BACKUP_DIR="$(pwd)/backups"
|
||||||
|
REMOTE_HOST="${BACKUP_SERVER_HOST}" # Remote server hostname/IP
|
||||||
|
REMOTE_USER="${BACKUP_SERVER_USER}" # Remote server username
|
||||||
|
REMOTE_BACKUP_DIR="${BACKUP_SERVER_PATH}" # Remote server backup path
|
||||||
|
TEMP_DIR="$BACKUP_DIR/tmp"
|
||||||
|
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
|
||||||
|
MODE=""
|
||||||
|
HOURLY_RETENTION=4
|
||||||
|
DAILY_RETENTION=3
|
||||||
|
WEEKLY_RETENTION=1
|
||||||
|
MIN_SPACE=20 # Minimum space in GB
|
||||||
|
DB_CONTAINER="mailserver_db"
|
||||||
|
|
||||||
|
# Function to display usage
|
||||||
|
function usage() {
|
||||||
|
echo "Usage: $0 --hourly | --daily | --weekly"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check available space and cleanup if necessary
|
||||||
|
function check_and_cleanup_space() {
|
||||||
|
local available_space=$(df -BG "$REMOTE_BACKUP_DIR" | awk 'NR==2 {gsub("G","",$4); print $4}')
|
||||||
|
|
||||||
|
if [ "$available_space" -le "$MIN_SPACE" ]; then
|
||||||
|
echo "Available space ($available_space GB) is less than minimum required ($MIN_SPACE GB)"
|
||||||
|
echo "Starting cleanup..."
|
||||||
|
|
||||||
|
while [ "$available_space" -le "$MIN_SPACE" ]; do
|
||||||
|
# Find oldest backup file
|
||||||
|
oldest_file=$(find "$REMOTE_BACKUP_DIR" -type f -name "*.zip" -printf '%T+ %p\n' | sort | head -n 1 | awk '{print $2}')
|
||||||
|
|
||||||
|
if [ -z "$oldest_file" ]; then
|
||||||
|
echo "No more files to delete!"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get file size before deletion for logging
|
||||||
|
file_size=$(du -h "$oldest_file" | cut -f1)
|
||||||
|
|
||||||
|
# Delete the file
|
||||||
|
rm -f "$oldest_file"
|
||||||
|
echo "Deleted old backup: $oldest_file (Size: $file_size)"
|
||||||
|
|
||||||
|
# Recalculate available space
|
||||||
|
available_space=$(df -BG "$REMOTE_BACKUP_DIR" | awk 'NR==2 {gsub("G","",$4); print $4}')
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Cleanup complete. Available space: $available_space GB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "##########################"
|
||||||
|
echo "Starting backup..."
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if the correct parameter is passed
|
||||||
|
if [[ "$1" == "--hourly" ]]; then
|
||||||
|
MODE="hourly"
|
||||||
|
elif [[ "$1" == "--daily" ]]; then
|
||||||
|
MODE="daily"
|
||||||
|
elif [[ "$1" == "--weekly" ]]; then
|
||||||
|
MODE="weekly"
|
||||||
|
else
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
echo "##########################"
|
||||||
|
echo "Mode: $MODE"
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "##########################"
|
||||||
|
echo "Creating backup directories..."
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create backup and temp directories if they don't exist
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
mkdir -p "/var/log/backup"
|
||||||
|
|
||||||
|
# Backup all MySQL databases
|
||||||
|
echo "##########################"
|
||||||
|
echo "Backing up MySQL databases..."
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test database connection first
|
||||||
|
if ! docker exec -i $DB_CONTAINER mariadb -u $MYSQL_USER -p${MYSQL_PASSWORD} -e "SELECT 1;" >/dev/null 2>&1; then
|
||||||
|
echo "Error: Cannot connect to MySQL database. Please check credentials."
|
||||||
|
echo "Container: $DB_CONTAINER"
|
||||||
|
echo "User: $MYSQL_USER"
|
||||||
|
echo "Password being used: ${MYSQL_PASSWORD}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get databases list without using -it flag (which requires terminal)
|
||||||
|
databases=$(docker exec -i $DB_CONTAINER mariadb -u $MYSQL_USER -p${MYSQL_PASSWORD} -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|mysql|sys)")
|
||||||
|
|
||||||
|
for db in $databases; do
|
||||||
|
echo "Backing up database: $db"
|
||||||
|
docker exec -i $DB_CONTAINER mariadb-dump -u $MYSQL_USER -p${MYSQL_PASSWORD} --databases "$db" > "$TEMP_DIR/${db}.sql"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
zip -j "$TEMP_DIR/${db}.sql.zip" "$TEMP_DIR/${db}.sql"
|
||||||
|
rm "$TEMP_DIR/${db}.sql"
|
||||||
|
else
|
||||||
|
echo "Error backing up database: $db"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
DIRECTORIES_TO_BACKUP=(
|
||||||
|
"/opt/2weekmail"
|
||||||
|
)
|
||||||
|
|
||||||
|
for dir in "${DIRECTORIES_TO_BACKUP[@]}"; do
|
||||||
|
DIR_NAME=$(basename "$dir")
|
||||||
|
zip -r "$TEMP_DIR/${DIR_NAME}_${MODE}_$TIMESTAMP.zip" "$dir" -x "*/node_modules/*" "*/backups/*"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Compress all SQL and directories into a single zip file
|
||||||
|
echo "##########################"
|
||||||
|
echo "Compressing backup files..."
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
FINAL_BACKUP_FILE="$BACKUP_DIR/${MODE}_backup_$TIMESTAMP.zip"
|
||||||
|
find "$TEMP_DIR" -name "*.zip" | while read file; do
|
||||||
|
zip -ur "$FINAL_BACKUP_FILE" "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Clean up temporary files
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
echo "##########################"
|
||||||
|
echo "Backup complete: $FINAL_BACKUP_FILE"
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
function apply_retention() {
|
||||||
|
backup_type=$1
|
||||||
|
retention_count=$2
|
||||||
|
|
||||||
|
# Find all backup files of the specified type, sort them by modification time, and keep the newest
|
||||||
|
backups=($(ls -t $BACKUP_DIR/${backup_type}_backup_*.zip))
|
||||||
|
|
||||||
|
# If the number of backups exceeds the retention limit, delete the older ones
|
||||||
|
if [ ${#backups[@]} -gt $retention_count ]; then
|
||||||
|
delete_count=$((${#backups[@]} - $retention_count))
|
||||||
|
for (( i=$retention_count; i<${#backups[@]}; i++ )); do
|
||||||
|
rm -f "${backups[$i]}"
|
||||||
|
echo "Deleted old $backup_type backup: ${backups[$i]}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "##########################"
|
||||||
|
echo "Applying retention policy..."
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Apply retention policy
|
||||||
|
if [[ "$MODE" == "hourly" ]]; then
|
||||||
|
apply_retention "hourly" $HOURLY_RETENTION
|
||||||
|
elif [[ "$MODE" == "daily" ]]; then
|
||||||
|
apply_retention "daily" $DAILY_RETENTION
|
||||||
|
elif [[ "$MODE" == "weekly" ]]; then
|
||||||
|
apply_retention "weekly" $WEEKLY_RETENTION
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Retention policy applied: $MODE backups cleaned."
|
||||||
|
echo "##########################"
|
||||||
|
echo "Backup cleanup complete."
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "##########################"
|
||||||
|
echo "Checking remote backup space..."
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check and cleanup space before copying new backup
|
||||||
|
check_and_cleanup_space
|
||||||
|
|
||||||
|
# Use rsync to copy the backup
|
||||||
|
if ! ssh -p "$BACKUP_SERVER_PORT" "${REMOTE_USER}@${REMOTE_HOST}" exit 2>/dev/null; then
|
||||||
|
echo "Error: Cannot connect to remote backup server"
|
||||||
|
echo "Backup file is saved locally at: $FINAL_BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync -av --progress -e "ssh -p $BACKUP_SERVER_PORT" "$FINAL_BACKUP_FILE" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BACKUP_DIR}/"
|
||||||
|
|
||||||
|
echo "##########################"
|
||||||
|
echo "Remote backup complete"
|
||||||
|
echo "##########################"
|
||||||
|
echo ""
|
||||||
BIN
backups/daily_backup_2025-03-23_13-09-21.zip
Normal file
BIN
backups/daily_backup_2025-03-23_13-09-21.zip
Normal file
Binary file not shown.
BIN
backups/daily_backup_2025-03-23_13-12-06.zip
Normal file
BIN
backups/daily_backup_2025-03-23_13-12-06.zip
Normal file
Binary file not shown.
BIN
backups/hourly_backup_2025-03-23_12-37-46.zip
Normal file
BIN
backups/hourly_backup_2025-03-23_12-37-46.zip
Normal file
Binary file not shown.
BIN
backups/hourly_backup_2025-03-23_12-41-36.zip
Normal file
BIN
backups/hourly_backup_2025-03-23_12-41-36.zip
Normal file
Binary file not shown.
BIN
backups/hourly_backup_2025-03-23_12-46-30.zip
Normal file
BIN
backups/hourly_backup_2025-03-23_12-46-30.zip
Normal file
Binary file not shown.
BIN
backups/hourly_backup_2025-03-23_12-47-03.zip
Normal file
BIN
backups/hourly_backup_2025-03-23_12-47-03.zip
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user