smtp integration and password recovery.
This commit is contained in:
parent
c0811de8fa
commit
52d921c141
@ -30,6 +30,7 @@ COPY config ./config
|
|||||||
COPY controllers ./controllers
|
COPY controllers ./controllers
|
||||||
COPY middleware ./middleware
|
COPY middleware ./middleware
|
||||||
COPY utils ./utils
|
COPY utils ./utils
|
||||||
|
COPY templates ./templates
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN addgroup -g 1001 -S nodejs && \
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
|||||||
60
map/app/controllers/passwordRecoveryController.js
Normal file
60
map/app/controllers/passwordRecoveryController.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
const nocodbService = require('../services/nocodb');
|
||||||
|
const { sendPasswordRecovery } = require('../services/email');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
class PasswordRecoveryController {
|
||||||
|
async requestPassword(req, res) {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Email address is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email format
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid email format'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Password recovery requested for: ${email}`);
|
||||||
|
|
||||||
|
// Find user in database
|
||||||
|
const user = await nocodbService.getUserByEmail(email);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// Don't reveal whether the email exists or not for security
|
||||||
|
logger.warn(`Password recovery attempted for non-existent email: ${email}`);
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'If an account exists with this email, you will receive your password shortly.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send password email
|
||||||
|
await sendPasswordRecovery(user);
|
||||||
|
|
||||||
|
logger.info(`Password recovery email sent to: ${email}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'If an account exists with this email, you will receive your password shortly.'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Password recovery error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to process password recovery request. Please try again later.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new PasswordRecoveryController();
|
||||||
32
map/app/package-lock.json
generated
32
map/app/package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
|
"nodemailer": "^7.0.5",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
},
|
},
|
||||||
@ -725,16 +726,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-session": {
|
"node_modules/express-session": {
|
||||||
"version": "1.18.1",
|
"version": "1.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||||
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
|
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "0.7.2",
|
"cookie": "0.7.2",
|
||||||
"cookie-signature": "1.0.7",
|
"cookie-signature": "1.0.7",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "~2.0.0",
|
"depd": "~2.0.0",
|
||||||
"on-headers": "~1.0.2",
|
"on-headers": "~1.1.0",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"uid-safe": "~2.1.5"
|
"uid-safe": "~2.1.5"
|
||||||
@ -834,10 +834,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
@ -1339,6 +1338,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
||||||
@ -1437,10 +1444,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/on-headers": {
|
"node_modules/on-headers": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
|
"nodemailer": "^7.0.5",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -136,6 +136,21 @@
|
|||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-recovery {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-recovery a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-recovery a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -180,6 +195,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="password-recovery">
|
||||||
|
<a href="#" id="forgot-password-link">Forgot your password?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="login-footer">
|
<div class="login-footer">
|
||||||
<p>Access is restricted to authorized users only. Please contact your system administrator for login details.</p>
|
<p>Access is restricted to authorized users only. Please contact your system administrator for login details.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -262,6 +281,45 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
|
// Add password recovery handler
|
||||||
|
document.getElementById('forgot-password-link').addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const email = prompt('Please enter your email address:');
|
||||||
|
if (!email) return;
|
||||||
|
|
||||||
|
const errorMessage = document.getElementById('error-message');
|
||||||
|
const successMessage = document.getElementById('success-message');
|
||||||
|
|
||||||
|
// Clear previous messages
|
||||||
|
errorMessage.classList.remove('show');
|
||||||
|
successMessage.classList.remove('show');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/recover-password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
successMessage.textContent = data.message;
|
||||||
|
successMessage.classList.add('show');
|
||||||
|
} else {
|
||||||
|
errorMessage.textContent = data.error || 'Failed to process request';
|
||||||
|
errorMessage.classList.add('show');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Password recovery error:', error);
|
||||||
|
errorMessage.textContent = 'Failed to send password recovery email';
|
||||||
|
errorMessage.classList.add('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Cache Management -->
|
<!-- Cache Management -->
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const authController = require('../controllers/authController');
|
const authController = require('../controllers/authController');
|
||||||
|
const passwordRecoveryController = require('../controllers/passwordRecoveryController');
|
||||||
const { authLimiter } = require('../middleware/rateLimiter');
|
const { authLimiter } = require('../middleware/rateLimiter');
|
||||||
|
|
||||||
// Login route with rate limiting
|
// Login route with rate limiting
|
||||||
router.post('/login', authLimiter, authController.login);
|
router.post('/login', authLimiter, authController.login);
|
||||||
|
|
||||||
|
// Password recovery route with rate limiting
|
||||||
|
router.post('/recover-password', authLimiter, passwordRecoveryController.requestPassword);
|
||||||
|
|
||||||
// Logout route
|
// Logout route
|
||||||
router.post('/logout', authController.logout);
|
router.post('/logout', authController.logout);
|
||||||
|
|
||||||
|
|||||||
@ -261,4 +261,22 @@ router.get('/login-structure', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reload email templates (development only)
|
||||||
|
router.get('/reload-email-templates', (req, res) => {
|
||||||
|
try {
|
||||||
|
const emailTemplates = require('../services/emailTemplates');
|
||||||
|
emailTemplates.clearCache();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Email template cache cleared'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error clearing email template cache:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to clear email template cache'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -12,6 +12,7 @@ const logger = require('./utils/logger');
|
|||||||
const { getCookieConfig } = require('./utils/helpers');
|
const { getCookieConfig } = require('./utils/helpers');
|
||||||
const { apiLimiter } = require('./middleware/rateLimiter');
|
const { apiLimiter } = require('./middleware/rateLimiter');
|
||||||
const { cacheBusting } = require('./utils/cacheBusting');
|
const { cacheBusting } = require('./utils/cacheBusting');
|
||||||
|
const { initializeEmailService } = require('./services/email');
|
||||||
|
|
||||||
// Initialize Express app
|
// Initialize Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -142,6 +143,9 @@ app.get('/api/docs-search', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize email service
|
||||||
|
initializeEmailService();
|
||||||
|
|
||||||
// Import and setup routes
|
// Import and setup routes
|
||||||
require('./routes')(app);
|
require('./routes')(app);
|
||||||
|
|
||||||
|
|||||||
99
map/app/services/email.js
Normal file
99
map/app/services/email.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const emailTemplates = require('./emailTemplates');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
// Create reusable transporter
|
||||||
|
let transporter;
|
||||||
|
|
||||||
|
const initializeEmailService = () => {
|
||||||
|
const emailConfig = {
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
port: parseInt(process.env.SMTP_PORT),
|
||||||
|
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Additional settings for specific providers
|
||||||
|
if (process.env.SMTP_SERVICE) {
|
||||||
|
emailConfig.service = process.env.SMTP_SERVICE; // 'gmail' for example
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle TLS for port 587
|
||||||
|
if (emailConfig.port === 587) {
|
||||||
|
emailConfig.secure = false;
|
||||||
|
emailConfig.requireTLS = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
transporter = nodemailer.createTransport(emailConfig);
|
||||||
|
|
||||||
|
// Verify connection
|
||||||
|
transporter.verify((error, success) => {
|
||||||
|
if (error) {
|
||||||
|
logger.error('SMTP connection failed:', error);
|
||||||
|
} else {
|
||||||
|
logger.info('SMTP server ready to send emails');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendEmail = async ({ to, subject, text, html }) => {
|
||||||
|
if (!transporter) {
|
||||||
|
throw new Error('Email service not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
html
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await transporter.sendMail(mailOptions);
|
||||||
|
logger.info(`Email sent: ${info.messageId}`);
|
||||||
|
return info;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send email:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendPasswordRecovery = async (user) => {
|
||||||
|
try {
|
||||||
|
const baseUrl = config.isProduction ?
|
||||||
|
`https://map.${config.domain}` :
|
||||||
|
`http://localhost:${config.port}`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
APP_NAME: process.env.APP_NAME || 'CMlite Map',
|
||||||
|
USER_NAME: user.Name || user.name || user.Email || user.email,
|
||||||
|
USER_EMAIL: user.Email || user.email,
|
||||||
|
PASSWORD: user.Password || user.password,
|
||||||
|
LOGIN_URL: `${baseUrl}/login.html`,
|
||||||
|
TIMESTAMP: new Date().toLocaleString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const { html, text } = await emailTemplates.render('password-recovery', variables);
|
||||||
|
|
||||||
|
return await sendEmail({
|
||||||
|
to: user.Email || user.email,
|
||||||
|
subject: `Password Recovery - ${variables.APP_NAME}`,
|
||||||
|
text,
|
||||||
|
html
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send password recovery email:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initializeEmailService,
|
||||||
|
sendEmail,
|
||||||
|
sendPasswordRecovery
|
||||||
|
};
|
||||||
69
map/app/services/emailTemplates.js
Normal file
69
map/app/services/emailTemplates.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
class EmailTemplateService {
|
||||||
|
constructor() {
|
||||||
|
this.templatesDir = path.join(__dirname, '../templates/email');
|
||||||
|
this.cache = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTemplate(templateName, type = 'html') {
|
||||||
|
const cacheKey = `${templateName}.${type}`;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (this.cache.has(cacheKey)) {
|
||||||
|
return this.cache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templatePath = path.join(this.templatesDir, `${templateName}.${type}`);
|
||||||
|
const template = await fs.readFile(templatePath, 'utf-8');
|
||||||
|
|
||||||
|
// Cache the template
|
||||||
|
this.cache.set(cacheKey, template);
|
||||||
|
|
||||||
|
return template;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to load email template ${templateName}.${type}:`, error);
|
||||||
|
throw new Error(`Email template not found: ${templateName}.${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemplate(template, variables) {
|
||||||
|
let rendered = template;
|
||||||
|
|
||||||
|
// Replace all {{VARIABLE}} with actual values
|
||||||
|
Object.entries(variables).forEach(([key, value]) => {
|
||||||
|
const regex = new RegExp(`{{${key}}}`, 'g');
|
||||||
|
rendered = rendered.replace(regex, value || '');
|
||||||
|
});
|
||||||
|
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(templateName, variables) {
|
||||||
|
try {
|
||||||
|
// Load both HTML and text versions
|
||||||
|
const [htmlTemplate, textTemplate] = await Promise.all([
|
||||||
|
this.loadTemplate(templateName, 'html'),
|
||||||
|
this.loadTemplate(templateName, 'txt')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
html: this.renderTemplate(htmlTemplate, variables),
|
||||||
|
text: this.renderTemplate(textTemplate, variables)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to render email template:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear template cache (useful for development)
|
||||||
|
clearCache() {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new EmailTemplateService();
|
||||||
79
map/app/templates/email/password-recovery.html
Normal file
79
map/app/templates/email/password-recovery.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Your Password</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 30px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
color: #a02c8d;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.password-box {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">{{APP_NAME}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>Password Recovery</h2>
|
||||||
|
<p>Hello {{USER_NAME}},</p>
|
||||||
|
<p>You requested your password for the account associated with <strong>{{USER_EMAIL}}</strong>.</p>
|
||||||
|
<p>Your password is:</p>
|
||||||
|
<div class="password-box">{{PASSWORD}}</div>
|
||||||
|
<p>You can use this password to log in at: <a href="{{LOGIN_URL}}">{{LOGIN_URL}}</a></p>
|
||||||
|
<p class="warning">⚠️ For security reasons, we recommend changing your password after logging in.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>This email was sent from {{APP_NAME}} at {{TIMESTAMP}}</p>
|
||||||
|
<p>If you didn't request this password, please contact your administrator immediately.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
map/app/templates/email/password-recovery.txt
Normal file
15
map/app/templates/email/password-recovery.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
Password Recovery - {{APP_NAME}}
|
||||||
|
|
||||||
|
Hello {{USER_NAME}},
|
||||||
|
|
||||||
|
You requested your password for the account associated with {{USER_EMAIL}}.
|
||||||
|
|
||||||
|
Your password is: {{PASSWORD}}
|
||||||
|
|
||||||
|
You can use this password to log in at: {{LOGIN_URL}}
|
||||||
|
|
||||||
|
WARNING: For security reasons, we recommend changing your password after logging in.
|
||||||
|
|
||||||
|
---
|
||||||
|
This email was sent from {{APP_NAME}} at {{TIMESTAMP}}
|
||||||
|
If you didn't request this password, please contact your administrator immediately.
|
||||||
Loading…
x
Reference in New Issue
Block a user