Updates to sending user details

This commit is contained in:
2025-08-03 13:26:20 -06:00
parent 6b4732db65
commit 86685a13a6
22 changed files with 1669 additions and 156 deletions

View File

@@ -2,6 +2,7 @@ const nocodbService = require('../services/nocodb');
const logger = require('../utils/logger');
const config = require('../config');
const { sanitizeUser, extractId } = require('../utils/helpers');
const { sendLoginDetails } = require('../services/email');
class UsersController {
async getAll(req, res) {
@@ -155,6 +156,47 @@ class UsersController {
});
}
}
async sendLoginDetails(req, res) {
try {
const userId = req.params.id;
if (!config.nocodb.loginSheetId) {
return res.status(500).json({
success: false,
error: 'Login sheet not configured'
});
}
// Get user data from database
const user = await nocodbService.getById(
config.nocodb.loginSheetId,
userId
);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
// Send login details email
await sendLoginDetails(user);
res.json({
success: true,
message: 'Login details sent successfully'
});
} catch (error) {
logger.error('Error sending login details:', error);
res.status(500).json({
success: false,
error: 'Failed to send login details'
});
}
}
}
module.exports = new UsersController();

View File

@@ -346,7 +346,7 @@ function setupEventListeners() {
}
// User form submission
const userForm = document.getElementById('user-form');
const userForm = document.getElementById('create-user-form');
if (userForm) {
userForm.addEventListener('submit', createUser);
}
@@ -1269,6 +1269,9 @@ function displayUsers(users) {
<td data-label="Created">${formattedDate}</td>
<td data-label="Actions">
<div class="user-actions">
<button class="btn btn-secondary send-login-btn" data-user-id="${userId}" data-user-email="${escapeHtml(user.email || user.Email)}">
Send Login Details
</button>
<button class="btn btn-danger delete-user-btn" data-user-id="${userId}" data-user-email="${escapeHtml(user.email || user.Email)}">
Delete
</button>
@@ -1288,22 +1291,27 @@ function displayUsers(users) {
}
function setupUserActionListeners() {
const tableBody = document.getElementById('users-table-body');
if (!tableBody) return;
const container = document.querySelector('.users-list');
if (!container) return;
// Remove existing listeners by cloning the node
const newTableBody = tableBody.cloneNode(true);
tableBody.parentNode.replaceChild(newTableBody, tableBody);
// Remove existing event listeners by cloning the container
const newContainer = container.cloneNode(true);
container.parentNode.replaceChild(newContainer, container);
// Get the updated reference
const updatedTableBody = document.getElementById('users-table-body');
const updatedContainer = document.querySelector('.users-list');
updatedTableBody.addEventListener('click', function(e) {
updatedContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-user-btn')) {
const userId = e.target.getAttribute('data-user-id');
const userEmail = e.target.getAttribute('data-user-email');
console.log('Delete button clicked for user:', userId);
deleteUser(userId, userEmail);
} else if (e.target.classList.contains('send-login-btn')) {
const userId = e.target.getAttribute('data-user-id');
const userEmail = e.target.getAttribute('data-user-email');
console.log('Send login details button clicked for user:', userId);
sendLoginDetailsToUser(userId, userEmail);
}
});
}
@@ -1333,13 +1341,40 @@ async function deleteUser(userId, userEmail) {
}
}
async function sendLoginDetailsToUser(userId, userEmail) {
if (!confirm(`Send login details to "${userEmail}"?`)) {
return;
}
try {
const response = await fetch(`/api/users/${userId}/send-login-details`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showStatus(`Login details sent to "${userEmail}" successfully`, 'success');
} else {
throw new Error(data.error || 'Failed to send login details');
}
} catch (error) {
console.error('Error sending login details:', error);
showStatus(`Failed to send login details: ${error.message}`, 'error');
}
}
async function createUser(e) {
e.preventDefault();
const email = document.getElementById('user-email').value.trim();
const password = document.getElementById('user-password').value;
const name = document.getElementById('user-name').value.trim();
const admin = document.getElementById('user-admin').checked;
const admin = document.getElementById('user-is-admin').checked;
if (!email || !password) {
showStatus('Email and password are required', 'error');
@@ -1382,7 +1417,7 @@ async function createUser(e) {
}
function clearUserForm() {
const form = document.getElementById('user-form');
const form = document.getElementById('create-user-form');
if (form) {
form.reset();
showStatus('User form cleared', 'info');

View File

@@ -8,6 +8,9 @@ router.get('/', usersController.getAll);
// Create new user
router.post('/', usersController.create);
// Send login details to user
router.post('/:id/send-login-details', usersController.sendLoginDetails);
// Delete user
router.delete('/:id', usersController.delete);

View File

@@ -92,8 +92,41 @@ const sendPasswordRecovery = async (user) => {
}
};
const sendLoginDetails = async (user) => {
try {
const baseUrl = config.isProduction ?
`https://map.${config.domain}` :
`http://localhost:${config.port}`;
const isAdmin = user.admin || user.Admin || false;
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,
USER_ROLE: isAdmin ? 'Administrator' : 'User',
LOGIN_URL: `${baseUrl}/login.html`,
TIMESTAMP: new Date().toLocaleString()
};
const { html, text } = await emailTemplates.render('login-details', variables);
return await sendEmail({
to: user.Email || user.email,
subject: `Your Login Details - ${variables.APP_NAME}`,
text,
html
});
} catch (error) {
logger.error('Failed to send login details email:', error);
throw error;
}
};
module.exports = {
initializeEmailService,
sendEmail,
sendPasswordRecovery
sendPasswordRecovery,
sendLoginDetails
};

View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Your Login Details</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;
}
.credentials-box {
background-color: #f0f0f0;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border: 1px solid #ddd;
}
.credential-item {
margin: 10px 0;
}
.credential-label {
font-weight: bold;
display: inline-block;
width: 100px;
}
.credential-value {
font-family: monospace;
font-size: 16px;
color: #2c3e50;
}
.login-button {
display: inline-block;
background-color: #a02c8d;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}
.footer {
text-align: center;
font-size: 12px;
color: #666;
margin-top: 30px;
}
.info {
color: #3498db;
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>Your Login Details</h2>
<p>Hello {{USER_NAME}},</p>
<p>Here are your login credentials for {{APP_NAME}}:</p>
<div class="credentials-box">
<div class="credential-item">
<span class="credential-label">Email:</span>
<span class="credential-value">{{USER_EMAIL}}</span>
</div>
<div class="credential-item">
<span class="credential-label">Password:</span>
<span class="credential-value">{{PASSWORD}}</span>
</div>
<div class="credential-item">
<span class="credential-label">Role:</span>
<span class="credential-value">{{USER_ROLE}}</span>
</div>
</div>
<p>You can log in using the link below:</p>
<p style="text-align: center;">
<a href="{{LOGIN_URL}}" class="login-button">Login to {{APP_NAME}}</a>
</p>
<p class="info">💡 For security reasons, we recommend changing your password after your first login.</p>
</div>
<div class="footer">
<p>This email was sent from {{APP_NAME}} at {{TIMESTAMP}}</p>
<p>If you have any questions, please contact your administrator.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
Login Details - {{APP_NAME}}
Hello {{USER_NAME}},
Here are your login credentials for {{APP_NAME}}:
Email: {{USER_EMAIL}}
Password: {{PASSWORD}}
Role: {{USER_ROLE}}
You can log in at: {{LOGIN_URL}}
For security reasons, we recommend changing your password after your first login.
---
This email was sent from {{APP_NAME}} at {{TIMESTAMP}}
If you have any questions, please contact your administrator.