318 lines
12 KiB
JavaScript
318 lines
12 KiB
JavaScript
import express from 'express';
|
|
import bcrypt from 'bcrypt';
|
|
import jwt from 'jsonwebtoken';
|
|
import { createPermissionHelpers } from './permissions.js';
|
|
|
|
export function createAuthRoutes({ pool }) {
|
|
const router = express.Router();
|
|
const { requirePermission, getUserPermissions } = createPermissionHelpers({ pool });
|
|
|
|
// Local authenticate(): used by user-management endpoints that need req.user populated
|
|
// with id/username/email/is_admin. NOT the per-service authenticate() — that lives in
|
|
// shared/auth/middleware.js and is used by downstream services. Auth-server's surface is
|
|
// small enough that a local copy is fine; the security boundary is the JWT verify step.
|
|
async function authenticate(req, res, next) {
|
|
try {
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
const token = authHeader.split(' ')[1];
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
const result = await pool.query(
|
|
'SELECT id, username, email, is_admin, rocket_chat_user_id FROM users WHERE id = $1',
|
|
[decoded.userId]
|
|
);
|
|
if (result.rows.length === 0) {
|
|
return res.status(401).json({ error: 'User not found' });
|
|
}
|
|
req.user = result.rows[0];
|
|
next();
|
|
} catch (error) {
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
}
|
|
|
|
router.post('/login', async (req, res) => {
|
|
try {
|
|
const { username, password } = req.body;
|
|
const result = await pool.query(
|
|
'SELECT id, username, password, is_admin, is_active, rocket_chat_user_id FROM users WHERE username = $1',
|
|
[username]
|
|
);
|
|
if (result.rows.length === 0) {
|
|
return res.status(401).json({ error: 'Invalid username or password' });
|
|
}
|
|
const user = result.rows[0];
|
|
if (!user.is_active) {
|
|
return res.status(403).json({ error: 'Account is inactive' });
|
|
}
|
|
const validPassword = await bcrypt.compare(password, user.password);
|
|
if (!validPassword) {
|
|
return res.status(401).json({ error: 'Invalid username or password' });
|
|
}
|
|
await pool.query(
|
|
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1',
|
|
[user.id]
|
|
);
|
|
const token = jwt.sign(
|
|
{ userId: user.id, username: user.username },
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: '8h' }
|
|
);
|
|
const permissions = await getUserPermissions(user.id);
|
|
res.json({
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
is_admin: user.is_admin,
|
|
rocket_chat_user_id: user.rocket_chat_user_id,
|
|
permissions,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
router.get('/me', authenticate, async (req, res) => {
|
|
try {
|
|
const permissions = await getUserPermissions(req.user.id);
|
|
res.json({
|
|
id: req.user.id,
|
|
username: req.user.username,
|
|
email: req.user.email,
|
|
is_admin: req.user.is_admin,
|
|
rocket_chat_user_id: req.user.rocket_chat_user_id,
|
|
permissions,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error getting current user:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
router.get('/users', authenticate, requirePermission('view:users'), async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT id, username, email, is_admin, is_active, rocket_chat_user_id, created_at, last_login
|
|
FROM users
|
|
ORDER BY username
|
|
`);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
console.error('Error getting users:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
router.get('/users/:id', authenticate, requirePermission('view:users'), async (req, res) => {
|
|
try {
|
|
const userId = req.params.id;
|
|
const userResult = await pool.query(`
|
|
SELECT id, username, email, is_admin, is_active, rocket_chat_user_id, created_at, last_login
|
|
FROM users
|
|
WHERE id = $1
|
|
`, [userId]);
|
|
if (userResult.rows.length === 0) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
const permissionsResult = await pool.query(`
|
|
SELECT p.id, p.name, p.code, p.category, p.description
|
|
FROM permissions p
|
|
JOIN user_permissions up ON p.id = up.permission_id
|
|
WHERE up.user_id = $1
|
|
ORDER BY p.category, p.name
|
|
`, [userId]);
|
|
res.json({
|
|
...userResult.rows[0],
|
|
permissions: permissionsResult.rows,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error getting user:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
router.post('/users', authenticate, requirePermission('create:users'), async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const { username, email, password, is_admin, is_active, rocket_chat_user_id, permissions } = req.body;
|
|
if (!username || !password) {
|
|
return res.status(400).json({ error: 'Username and password are required' });
|
|
}
|
|
const existingUser = await client.query(
|
|
'SELECT id FROM users WHERE username = $1',
|
|
[username]
|
|
);
|
|
if (existingUser.rows.length > 0) {
|
|
return res.status(400).json({ error: 'Username already exists' });
|
|
}
|
|
await client.query('BEGIN');
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
const rcUserId = rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null;
|
|
const userResult = await client.query(`
|
|
INSERT INTO users (username, email, password, is_admin, is_active, rocket_chat_user_id, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
|
|
RETURNING id
|
|
`, [username, email || null, hashedPassword, !!is_admin, is_active !== false, rcUserId]);
|
|
const userId = userResult.rows[0].id;
|
|
|
|
if (!is_admin && Array.isArray(permissions) && permissions.length > 0) {
|
|
const permissionIds = normalizePermissionIds(permissions);
|
|
if (permissionIds.length > 0) {
|
|
await client.query(
|
|
`INSERT INTO user_permissions (user_id, permission_id)
|
|
SELECT $1, unnest($2::int[])
|
|
ON CONFLICT DO NOTHING`,
|
|
[userId, permissionIds]
|
|
);
|
|
}
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
res.status(201).json({ id: userId, message: 'User created successfully' });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
console.error('Error creating user:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
router.put('/users/:id', authenticate, requirePermission('edit:users'), async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const userId = req.params.id;
|
|
const { username, email, password, is_admin, is_active, rocket_chat_user_id, permissions } = req.body;
|
|
|
|
const userExists = await client.query('SELECT id FROM users WHERE id = $1', [userId]);
|
|
if (userExists.rows.length === 0) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
await client.query('BEGIN');
|
|
|
|
const updateFields = [];
|
|
const updateValues = [userId];
|
|
let paramIndex = 2;
|
|
|
|
if (username !== undefined) { updateFields.push(`username = $${paramIndex++}`); updateValues.push(username); }
|
|
if (email !== undefined) { updateFields.push(`email = $${paramIndex++}`); updateValues.push(email || null); }
|
|
if (is_admin !== undefined) { updateFields.push(`is_admin = $${paramIndex++}`); updateValues.push(!!is_admin); }
|
|
if (is_active !== undefined) { updateFields.push(`is_active = $${paramIndex++}`); updateValues.push(!!is_active); }
|
|
if (rocket_chat_user_id !== undefined) {
|
|
updateFields.push(`rocket_chat_user_id = $${paramIndex++}`);
|
|
updateValues.push(rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null);
|
|
}
|
|
if (password) {
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
updateFields.push(`password = $${paramIndex++}`);
|
|
updateValues.push(hashedPassword);
|
|
}
|
|
|
|
if (updateFields.length > 0) {
|
|
updateFields.push(`updated_at = CURRENT_TIMESTAMP`);
|
|
await client.query(`
|
|
UPDATE users SET ${updateFields.join(', ')} WHERE id = $1
|
|
`, updateValues);
|
|
}
|
|
|
|
if (Array.isArray(permissions)) {
|
|
await client.query('DELETE FROM user_permissions WHERE user_id = $1', [userId]);
|
|
const newIsAdmin = is_admin !== undefined
|
|
? is_admin
|
|
: (await client.query('SELECT is_admin FROM users WHERE id = $1', [userId])).rows[0].is_admin;
|
|
|
|
if (!newIsAdmin && permissions.length > 0) {
|
|
const permissionIds = normalizePermissionIds(permissions);
|
|
if (permissionIds.length > 0) {
|
|
await client.query(
|
|
`INSERT INTO user_permissions (user_id, permission_id)
|
|
SELECT $1, unnest($2::int[])
|
|
ON CONFLICT DO NOTHING`,
|
|
[userId, permissionIds]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
res.json({ message: 'User updated successfully' });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
console.error('Error updating user:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
router.delete('/users/:id', authenticate, requirePermission('delete:users'), async (req, res) => {
|
|
try {
|
|
const userId = req.params.id;
|
|
if (req.user.id === parseInt(userId, 10)) {
|
|
return res.status(400).json({ error: 'Cannot delete your own account' });
|
|
}
|
|
const result = await pool.query(
|
|
'DELETE FROM users WHERE id = $1 RETURNING id',
|
|
[userId]
|
|
);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
res.json({ message: 'User deleted successfully' });
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
router.get('/permissions/categories', authenticate, requirePermission('view:users'), async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT category, json_agg(
|
|
json_build_object(
|
|
'id', id, 'name', name, 'code', code, 'description', description
|
|
) ORDER BY name
|
|
) as permissions
|
|
FROM permissions
|
|
GROUP BY category
|
|
ORDER BY category
|
|
`);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
console.error('Error getting permissions:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
router.get('/permissions', authenticate, requirePermission('view:users'), async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT * FROM permissions ORDER BY category, name
|
|
`);
|
|
res.json(result.rows);
|
|
} catch (error) {
|
|
console.error('Error getting permissions:', error);
|
|
res.status(500).json({ error: 'Server error' });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
function normalizePermissionIds(permissions) {
|
|
return permissions
|
|
.map((p) => {
|
|
if (typeof p === 'object' && p?.id) return parseInt(p.id, 10);
|
|
if (typeof p === 'number') return p;
|
|
if (typeof p === 'string' && !Number.isNaN(parseInt(p, 10))) return parseInt(p, 10);
|
|
return null;
|
|
})
|
|
.filter((id) => id !== null && !Number.isNaN(id));
|
|
}
|