feat: User management, SMTP email activation and password resets
Build and Deploy / build-and-push (push) Successful in 1m0s
Build and Deploy / build-and-push (push) Successful in 1m0s
This commit is contained in:
@@ -44,6 +44,18 @@ const initDB = async () => {
|
||||
// Ignore if it already exists
|
||||
}
|
||||
|
||||
try {
|
||||
await client.query(`ALTER TABLE profiles ADD COLUMN activation_token TEXT`);
|
||||
} catch (err) {
|
||||
// Ignore if it already exists
|
||||
}
|
||||
|
||||
try {
|
||||
await client.query(`ALTER TABLE profiles ADD COLUMN reset_token TEXT`);
|
||||
} catch (err) {
|
||||
// Ignore if it already exists
|
||||
}
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
|
||||
@@ -40,7 +40,35 @@ const sendMail = async (to, subject, text, html = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendActivationEmail = async (email, token, baseUrl) => {
|
||||
const activationLink = `${baseUrl}/activate?token=${token}`;
|
||||
const subject = "Activate your Titan 3D Account";
|
||||
const html = `
|
||||
<h2>Welcome to Titan 3D!</h2>
|
||||
<p>Please click the link below to activate your account:</p>
|
||||
<a href="${activationLink}">${activationLink}</a>
|
||||
<p>If you did not request this, please ignore this email.</p>
|
||||
`;
|
||||
const text = `Welcome to Titan 3D! Please navigate to the following link to activate your account: ${activationLink}`;
|
||||
return sendMail(email, subject, text, html);
|
||||
};
|
||||
|
||||
const sendPasswordResetEmail = async (email, token, baseUrl) => {
|
||||
const resetLink = `${baseUrl}/reset-password?token=${token}`;
|
||||
const subject = "Reset your Titan 3D Password";
|
||||
const html = `
|
||||
<h2>Password Reset Request</h2>
|
||||
<p>Please click the link below to securely reset your password:</p>
|
||||
<a href="${resetLink}">${resetLink}</a>
|
||||
<p>If you did not request this, please ignore this email.</p>
|
||||
`;
|
||||
const text = `Please navigate to the following link to reset your password: ${resetLink}`;
|
||||
return sendMail(email, subject, text, html);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sendMail,
|
||||
sendActivationEmail,
|
||||
sendPasswordResetEmail,
|
||||
transporter
|
||||
};
|
||||
|
||||
+143
-4
@@ -11,8 +11,10 @@ const pdfParse = require('pdf-parse');
|
||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||
const axios = require('axios');
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const { db, run, get, all } = require('./database');
|
||||
const { sendMail } = require('./mail');
|
||||
const { sendMail, sendActivationEmail, sendPasswordResetEmail } = require('./mail');
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
@@ -70,13 +72,26 @@ app.post('/api/auth/register', async (req, res) => {
|
||||
const role = parseInt(countRow.count) === 0 ? 'admin' : 'user';
|
||||
const is_active = parseInt(countRow.count) === 0 ? true : false;
|
||||
|
||||
let activation_token = null;
|
||||
if (!is_active) {
|
||||
activation_token = crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
await run(
|
||||
'INSERT INTO profiles (id, email, password_hash, role, api_credits, storage_limit_mb, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, email, hash, role, 10, 500, is_active]
|
||||
'INSERT INTO profiles (id, email, password_hash, role, api_credits, storage_limit_mb, is_active, activation_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, email, hash, role, 10, 500, is_active, activation_token]
|
||||
);
|
||||
|
||||
if (is_active === false) {
|
||||
return res.json({ status: 'pending', message: 'Account created. Please contact an administrator to activate your account.' });
|
||||
// Send activation email
|
||||
const baseUrl = req.headers.origin || 'http://localhost:5173';
|
||||
try {
|
||||
await sendActivationEmail(email, activation_token, baseUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to send activation email', err);
|
||||
// We still return pending, maybe they can resend later
|
||||
}
|
||||
return res.json({ status: 'pending', message: 'Account created. Please check your email for the activation link.' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ id, email, role }, JWT_SECRET, { expiresIn: '7d' });
|
||||
@@ -87,6 +102,21 @@ app.post('/api/auth/register', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 1.5. Auth Activate
|
||||
app.post('/api/auth/activate', async (req, res) => {
|
||||
const { token } = req.body;
|
||||
if (!token) return res.status(400).json({ error: 'Activation token required' });
|
||||
try {
|
||||
const user = await get('SELECT * FROM profiles WHERE activation_token = ?', [token]);
|
||||
if (!user) return res.status(400).json({ error: 'Invalid or expired activation token' });
|
||||
|
||||
await run('UPDATE profiles SET is_active = true, activation_token = NULL WHERE id = ?', [user.id]);
|
||||
res.json({ success: true, message: 'Account activated successfully.' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Auth Login
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
@@ -221,6 +251,53 @@ app.get('/api/users', authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 7.1. Add user (Admin only)
|
||||
app.post('/api/users', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' });
|
||||
const { email, role, api_credits, storage_limit_mb } = req.body;
|
||||
if (!email) return res.status(400).json({ error: 'Email required' });
|
||||
|
||||
try {
|
||||
const existing = await get('SELECT * FROM profiles WHERE email = ?', [email]);
|
||||
if (existing) return res.status(400).json({ error: 'User already exists' });
|
||||
|
||||
// Generate random temp password (they will reset it anyway)
|
||||
const tempPassword = crypto.randomBytes(16).toString('hex');
|
||||
const hash = await bcrypt.hash(tempPassword, 10);
|
||||
const id = uuidv4();
|
||||
const activation_token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
await run(
|
||||
'INSERT INTO profiles (id, email, password_hash, role, api_credits, storage_limit_mb, is_active, activation_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, email, hash, role || 'user', api_credits || 10, storage_limit_mb || 500, false, activation_token]
|
||||
);
|
||||
|
||||
const baseUrl = req.headers.origin || 'http://localhost:5173';
|
||||
try {
|
||||
await sendActivationEmail(email, activation_token, baseUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to send activation email', err);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'User created and activation email sent.' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 7.2. Delete user (Admin only)
|
||||
app.delete('/api/users/:id', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' });
|
||||
try {
|
||||
// Cascade delete projects for the user
|
||||
await run('DELETE FROM projects WHERE user_id = ?', [req.params.id]);
|
||||
await run('DELETE FROM profiles WHERE id = ?', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 8. Update user (Admin only)
|
||||
app.put('/api/users/:id', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' });
|
||||
@@ -233,6 +310,68 @@ app.put('/api/users/:id', authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 8.1. Admin trigger reset password for user
|
||||
app.post('/api/users/:id/reset-password', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' });
|
||||
try {
|
||||
const user = await get('SELECT * FROM profiles WHERE id = ?', [req.params.id]);
|
||||
if (!user) return res.status(404).json({ error: 'User not found' });
|
||||
|
||||
const reset_token = crypto.randomBytes(32).toString('hex');
|
||||
await run('UPDATE profiles SET reset_token = ? WHERE id = ?', [reset_token, user.id]);
|
||||
|
||||
const baseUrl = req.headers.origin || 'http://localhost:5173';
|
||||
try {
|
||||
await sendPasswordResetEmail(user.email, reset_token, baseUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to send reset email', err);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Password reset email sent to user.' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 8.2. Forgot Password (User request)
|
||||
app.post('/api/auth/forgot-password', async (req, res) => {
|
||||
const { email } = req.body;
|
||||
if (!email) return res.status(400).json({ error: 'Email required' });
|
||||
try {
|
||||
const user = await get('SELECT * FROM profiles WHERE email = ?', [email]);
|
||||
if (user) {
|
||||
const reset_token = crypto.randomBytes(32).toString('hex');
|
||||
await run('UPDATE profiles SET reset_token = ? WHERE id = ?', [reset_token, user.id]);
|
||||
const baseUrl = req.headers.origin || 'http://localhost:5173';
|
||||
try {
|
||||
await sendPasswordResetEmail(user.email, reset_token, baseUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to send reset email', err);
|
||||
}
|
||||
}
|
||||
// Always return success to prevent email enumeration
|
||||
res.json({ success: true, message: 'If an account exists, a reset link has been sent.' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 8.3. Reset Password (User submission)
|
||||
app.post('/api/auth/reset-password', async (req, res) => {
|
||||
const { token, newPassword } = req.body;
|
||||
if (!token || !newPassword) return res.status(400).json({ error: 'Token and new password required' });
|
||||
try {
|
||||
const user = await get('SELECT * FROM profiles WHERE reset_token = ?', [token]);
|
||||
if (!user) return res.status(400).json({ error: 'Invalid or expired reset token' });
|
||||
|
||||
const hash = await bcrypt.hash(newPassword, 10);
|
||||
await run('UPDATE profiles SET password_hash = ?, reset_token = NULL WHERE id = ?', [hash, user.id]);
|
||||
res.json({ success: true, message: 'Password has been reset successfully.' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 9. Deduct API credit
|
||||
app.post('/api/users/deduct-credit', authenticate, async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user