moxie-backend/src/routes/users.js
Z User 9b4d3242e2 Switch to Auth0 authentication
- 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
2026-03-27 22:19:15 +00:00

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;