- Replace custom session-based auth with Auth0 JWT validation - Add express-oauth2-jwt-bearer for token validation - Update database schema to support Auth0 users (auth0_id, picture fields) - Add Auth0 login/callback/logout endpoints - Auto-create users on first Auth0 login - Update user routes for Auth0 integration - Add dotenv for environment configuration - Update documentation with Auth0 setup instructions
365 lines
11 KiB
JavaScript
365 lines
11 KiB
JavaScript
import express from 'express';
|
|
import { dbRun, dbGet, dbAll } from '../db/database.js';
|
|
import { hashPassword, authenticateToken, optionalAuth, requireAdmin, requireActiveUser } from '../middleware/auth.js';
|
|
import { generateId, sanitizeUser, getPagination, ApiResponse, asyncHandler } from '../utils/helpers.js';
|
|
|
|
const router = express.Router();
|
|
|
|
// ============================================
|
|
// Public Routes
|
|
// ============================================
|
|
|
|
/**
|
|
* @route GET /api/users/me
|
|
* @desc Get current user profile (from Auth0 JWT)
|
|
* @access Private (requires Auth0 token)
|
|
*/
|
|
router.get('/me', authenticateToken, asyncHandler(async (req, res) => {
|
|
const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.user.id]);
|
|
if (!user) {
|
|
return res.status(404).json(ApiResponse(false, null, 'User not found'));
|
|
}
|
|
res.json(ApiResponse(true, { user: sanitizeUser(user) }));
|
|
}));
|
|
|
|
/**
|
|
* @route PUT /api/users/me
|
|
* @desc Update current user profile
|
|
* @access Private (requires Auth0 token)
|
|
*/
|
|
router.put('/me', authenticateToken, asyncHandler(async (req, res) => {
|
|
const { name, picture } = req.body;
|
|
const updates = [];
|
|
const values = [];
|
|
|
|
if (name !== undefined) {
|
|
updates.push('name = ?');
|
|
values.push(name);
|
|
}
|
|
|
|
if (picture !== undefined) {
|
|
updates.push('picture = ?');
|
|
values.push(picture);
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
return res.status(400).json(ApiResponse(false, null, 'No fields to update'));
|
|
}
|
|
|
|
updates.push('updated_at = CURRENT_TIMESTAMP');
|
|
values.push(req.user.id);
|
|
|
|
await dbRun(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, values);
|
|
|
|
const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.user.id]);
|
|
res.json(ApiResponse(true, { user: sanitizeUser(user) }, 'Profile updated successfully'));
|
|
}));
|
|
|
|
/**
|
|
* @route DELETE /api/users/me
|
|
* @desc Delete user account (soft delete by deactivating)
|
|
* @access Private (requires Auth0 token)
|
|
*/
|
|
router.delete('/me', authenticateToken, asyncHandler(async (req, res) => {
|
|
// Soft delete - just deactivate the account
|
|
await dbRun('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]);
|
|
|
|
// Delete all API keys
|
|
await dbRun('DELETE FROM api_keys WHERE user_id = ?', [req.user.id]);
|
|
|
|
// Delete all sessions
|
|
await dbRun('DELETE FROM sessions WHERE user_id = ?', [req.user.id]);
|
|
|
|
res.json(ApiResponse(true, null, 'Account deactivated successfully'));
|
|
}));
|
|
|
|
// ============================================
|
|
// Credits Routes
|
|
// ============================================
|
|
|
|
/**
|
|
* @route GET /api/users/credits
|
|
* @desc Get user credits and transaction history
|
|
* @access Private (requires Auth0 token)
|
|
*/
|
|
router.get('/credits', authenticateToken, asyncHandler(async (req, res) => {
|
|
const user = await dbGet('SELECT credits FROM users WHERE id = ?', [req.user.id]);
|
|
|
|
const { page, limit, offset } = getPagination(req.query.page, req.query.limit);
|
|
|
|
const transactions = await dbAll(
|
|
`SELECT * FROM credit_transactions
|
|
WHERE user_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[req.user.id, limit, offset]
|
|
);
|
|
|
|
const totalResult = await dbGet(
|
|
'SELECT COUNT(*) as count FROM credit_transactions WHERE user_id = ?',
|
|
[req.user.id]
|
|
);
|
|
|
|
res.json(ApiResponse(true, {
|
|
credits: user.credits,
|
|
transactions,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: totalResult.count,
|
|
totalPages: Math.ceil(totalResult.count / limit)
|
|
}
|
|
}));
|
|
}));
|
|
|
|
// ============================================
|
|
// API Keys Routes
|
|
// ============================================
|
|
|
|
/**
|
|
* @route GET /api/users/api-keys
|
|
* @desc Get user API keys
|
|
* @access Private (requires Auth0 token)
|
|
*/
|
|
router.get('/api-keys', authenticateToken, asyncHandler(async (req, res) => {
|
|
const keys = await dbAll(
|
|
`SELECT id, name, last_used, created_at, is_active
|
|
FROM api_keys
|
|
WHERE user_id = ?
|
|
ORDER BY created_at DESC`,
|
|
[req.user.id]
|
|
);
|
|
|
|
res.json(ApiResponse(true, { keys }));
|
|
}));
|
|
|
|
/**
|
|
* @route POST /api/users/api-keys
|
|
* @desc Create a new API key
|
|
* @access Private (requires Auth0 token)
|
|
*/
|
|
router.post('/api-keys', authenticateToken, asyncHandler(async (req, res) => {
|
|
const { name } = req.body;
|
|
|
|
// Check if user already has max API keys (optional limit)
|
|
const existingKeys = await dbGet(
|
|
'SELECT COUNT(*) as count FROM api_keys WHERE user_id = ? AND is_active = 1',
|
|
[req.user.id]
|
|
);
|
|
|
|
if (existingKeys.count >= 10) {
|
|
return res.status(400).json(ApiResponse(false, null, 'Maximum of 10 API keys allowed'));
|
|
}
|
|
|
|
const keyId = generateId();
|
|
const keyValue = `moxie_${generateId()}_${generateId()}`;
|
|
const keyHash = await hashPassword(keyValue);
|
|
|
|
await dbRun(
|
|
`INSERT INTO api_keys (id, user_id, key_hash, name)
|
|
VALUES (?, ?, ?, ?)`,
|
|
[keyId, req.user.id, keyHash, name || 'API Key']
|
|
);
|
|
|
|
// Return the key value only once (can't be retrieved later)
|
|
res.status(201).json(ApiResponse(true, {
|
|
key: {
|
|
id: keyId,
|
|
name: name || 'API Key',
|
|
key: keyValue
|
|
}
|
|
}, 'API key created. Save this key - it cannot be retrieved again.'));
|
|
}));
|
|
|
|
/**
|
|
* @route DELETE /api/users/api-keys/:keyId
|
|
* @desc Revoke an API key
|
|
* @access Private (requires Auth0 token)
|
|
*/
|
|
router.delete('/api-keys/:keyId', authenticateToken, asyncHandler(async (req, res) => {
|
|
const result = await dbRun(
|
|
'DELETE FROM api_keys WHERE id = ? AND user_id = ?',
|
|
[req.params.keyId, req.user.id]
|
|
);
|
|
|
|
if (result.changes === 0) {
|
|
return res.status(404).json(ApiResponse(false, null, 'API key not found'));
|
|
}
|
|
|
|
res.json(ApiResponse(true, null, 'API key revoked'));
|
|
}));
|
|
|
|
// ============================================
|
|
// Admin Routes
|
|
// ============================================
|
|
|
|
/**
|
|
* @route GET /api/users
|
|
* @desc List all users (admin)
|
|
* @access Admin
|
|
*/
|
|
router.get('/', authenticateToken, requireAdmin, asyncHandler(async (req, res) => {
|
|
const { page, limit, offset } = getPagination(req.query.page, req.query.limit);
|
|
const search = req.query.search;
|
|
|
|
let whereClause = '';
|
|
let params = [];
|
|
|
|
if (search) {
|
|
whereClause = 'WHERE email LIKE ? OR name LIKE ?';
|
|
const searchPattern = `%${search}%`;
|
|
params = [searchPattern, searchPattern];
|
|
}
|
|
|
|
const users = await dbAll(
|
|
`SELECT id, auth0_id, email, name, picture, role, credits, subscription_status, subscription_tier,
|
|
is_active, created_at, last_login
|
|
FROM users
|
|
${whereClause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, limit, offset]
|
|
);
|
|
|
|
const totalResult = await dbGet(`SELECT COUNT(*) as count FROM users ${whereClause}`, params);
|
|
|
|
res.json(ApiResponse(true, {
|
|
users,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: totalResult.count,
|
|
totalPages: Math.ceil(totalResult.count / limit)
|
|
}
|
|
}));
|
|
}));
|
|
|
|
/**
|
|
* @route GET /api/users/:userId
|
|
* @desc Get user by ID (admin)
|
|
* @access Admin
|
|
*/
|
|
router.get('/:userId', authenticateToken, requireAdmin, asyncHandler(async (req, res) => {
|
|
const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.params.userId]);
|
|
|
|
if (!user) {
|
|
return res.status(404).json(ApiResponse(false, null, 'User not found'));
|
|
}
|
|
|
|
res.json(ApiResponse(true, { user: sanitizeUser(user) }));
|
|
}));
|
|
|
|
/**
|
|
* @route PUT /api/users/:userId
|
|
* @desc Update user by ID (admin)
|
|
* @access Admin
|
|
*/
|
|
router.put('/:userId', authenticateToken, requireAdmin, asyncHandler(async (req, res) => {
|
|
const { name, role, credits, subscription_status, subscription_tier, is_active, email } = req.body;
|
|
const updates = [];
|
|
const values = [];
|
|
|
|
if (name !== undefined) { updates.push('name = ?'); values.push(name); }
|
|
if (role !== undefined) { updates.push('role = ?'); values.push(role); }
|
|
if (credits !== undefined) { updates.push('credits = ?'); values.push(credits); }
|
|
if (subscription_status !== undefined) { updates.push('subscription_status = ?'); values.push(subscription_status); }
|
|
if (subscription_tier !== undefined) { updates.push('subscription_tier = ?'); values.push(subscription_tier); }
|
|
if (is_active !== undefined) { updates.push('is_active = ?'); values.push(is_active ? 1 : 0); }
|
|
if (email !== undefined) { updates.push('email = ?'); values.push(email); }
|
|
|
|
if (updates.length === 0) {
|
|
return res.status(400).json(ApiResponse(false, null, 'No fields to update'));
|
|
}
|
|
|
|
updates.push('updated_at = CURRENT_TIMESTAMP');
|
|
values.push(req.params.userId);
|
|
|
|
await dbRun(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, values);
|
|
|
|
const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.params.userId]);
|
|
res.json(ApiResponse(true, { user: sanitizeUser(user) }, 'User updated successfully'));
|
|
}));
|
|
|
|
/**
|
|
* @route DELETE /api/users/:userId
|
|
* @desc Delete user by ID (admin) - hard delete
|
|
* @access Admin
|
|
*/
|
|
router.delete('/:userId', authenticateToken, requireAdmin, asyncHandler(async (req, res) => {
|
|
const result = await dbRun('DELETE FROM users WHERE id = ?', [req.params.userId]);
|
|
|
|
if (result.changes === 0) {
|
|
return res.status(404).json(ApiResponse(false, null, 'User not found'));
|
|
}
|
|
|
|
res.json(ApiResponse(true, null, 'User deleted successfully'));
|
|
}));
|
|
|
|
/**
|
|
* @route POST /api/users/:userId/credits
|
|
* @desc Add or remove credits from user (admin)
|
|
* @access Admin
|
|
*/
|
|
router.post('/:userId/credits', authenticateToken, requireAdmin, asyncHandler(async (req, res) => {
|
|
const { amount, description } = req.body;
|
|
|
|
if (!amount || typeof amount !== 'number') {
|
|
return res.status(400).json(ApiResponse(false, null, 'Valid amount is required'));
|
|
}
|
|
|
|
const user = await dbGet('SELECT credits FROM users WHERE id = ?', [req.params.userId]);
|
|
if (!user) {
|
|
return res.status(404).json(ApiResponse(false, null, 'User not found'));
|
|
}
|
|
|
|
const newBalance = user.credits + amount;
|
|
if (newBalance < 0) {
|
|
return res.status(400).json(ApiResponse(false, null, 'Insufficient credits'));
|
|
}
|
|
|
|
// Update user credits
|
|
await dbRun('UPDATE users SET credits = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [newBalance, req.params.userId]);
|
|
|
|
// Record transaction
|
|
const transactionId = generateId();
|
|
await dbRun(
|
|
`INSERT INTO credit_transactions (id, user_id, amount, type, description)
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
[transactionId, req.params.userId, amount, amount > 0 ? 'credit' : 'debit', description || 'Admin adjustment']
|
|
);
|
|
|
|
res.json(ApiResponse(true, {
|
|
previousBalance: user.credits,
|
|
amount,
|
|
newBalance,
|
|
transactionId
|
|
}, 'Credits updated successfully'));
|
|
}));
|
|
|
|
/**
|
|
* @route PUT /api/users/:userId/role
|
|
* @desc Change user role (admin)
|
|
* @access Admin
|
|
*/
|
|
router.put('/:userId/role', authenticateToken, requireAdmin, asyncHandler(async (req, res) => {
|
|
const { role } = req.body;
|
|
|
|
if (!role || !['user', 'admin'].includes(role)) {
|
|
return res.status(400).json(ApiResponse(false, null, 'Valid role is required (user or admin)'));
|
|
}
|
|
|
|
const result = await dbRun(
|
|
'UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[role, req.params.userId]
|
|
);
|
|
|
|
if (result.changes === 0) {
|
|
return res.status(404).json(ApiResponse(false, null, 'User not found'));
|
|
}
|
|
|
|
const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.params.userId]);
|
|
res.json(ApiResponse(true, { user: sanitizeUser(user) }, 'User role updated successfully'));
|
|
}));
|
|
|
|
export default router;
|