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:
+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