From 08e2664a9aa6b3a6f71580a48d55460efaf479c9 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 22 Jul 2025 07:36:37 -0500 Subject: [PATCH] Fix admin users API endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 Convert TypeScript to JavaScript: - Replace api/admin/users.ts with api/admin/users.js - Use req, res pattern instead of NextRequest/NextResponse - Fix req.headers.get() -> req.headers.authorization - Use url.parse() for query parameters - Support GET (list users), PUT (update), DELETE (deactivate) - Proper admin authentication and error handling Resolves: req.headers.get is not a function error --- api/admin/users.js | 214 +++++++++++++++++++++++++++++++++++ api/admin/users.ts | 232 -------------------------------------- src/components/Navbar.tsx | 4 +- 3 files changed, 216 insertions(+), 234 deletions(-) create mode 100644 api/admin/users.js delete mode 100644 api/admin/users.ts diff --git a/api/admin/users.js b/api/admin/users.js new file mode 100644 index 0000000..d373b5a --- /dev/null +++ b/api/admin/users.js @@ -0,0 +1,214 @@ +const { Pool } = require('pg'); +const jwt = require('jsonwebtoken'); +const url = require('url'); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +}); + +// Middleware to verify admin access +function verifyAdmin(req) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new Error('No token provided'); + } + + const token = authHeader.substring(7); + const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production'; + + try { + const decoded = jwt.verify(token, jwtSecret); + + // Check if user has admin role + if (!decoded.roles || !decoded.roles.includes('admin')) { + throw new Error('Admin access required'); + } + + return decoded; + } catch (error) { + throw new Error('Invalid token or insufficient permissions'); + } +} + +export default async function handler(req, res) { + const client = await pool.connect(); + + try { + // Verify admin access + verifyAdmin(req); + + if (req.method === 'GET') { + // Parse query parameters + const parsedUrl = url.parse(req.url, true); + const query = parsedUrl.query; + + const page = parseInt(query.page || '1'); + const limit = parseInt(query.limit || '20'); + const search = query.search || ''; + const offset = (page - 1) * limit; + + let whereClause = ''; + let queryParams = [limit, offset]; + + if (search) { + whereClause = 'WHERE u.username ILIKE $3 OR u.email ILIKE $3 OR u.first_name ILIKE $3 OR u.last_name ILIKE $3'; + queryParams.push(`%${search}%`); + } + + // Get users with their roles + const usersQuery = ` + SELECT + u.id, u.username, u.email, u.first_name, u.last_name, + u.is_active, u.created_at, u.last_login, + ARRAY_AGG(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) as roles + FROM users u + LEFT JOIN user_roles ur ON u.id = ur.user_id + LEFT JOIN roles r ON ur.role_id = r.id + ${whereClause} + GROUP BY u.id, u.username, u.email, u.first_name, u.last_name, u.is_active, u.created_at, u.last_login + ORDER BY u.created_at DESC + LIMIT $1 OFFSET $2 + `; + + const usersResult = await client.query(usersQuery, queryParams); + + // Get total count + let countQuery = 'SELECT COUNT(*) FROM users u'; + let countParams = []; + + if (search) { + countQuery += ' WHERE u.username ILIKE $1 OR u.email ILIKE $1 OR u.first_name ILIKE $1 OR u.last_name ILIKE $1'; + countParams.push(`%${search}%`); + } + + const countResult = await client.query(countQuery, countParams); + const total = parseInt(countResult.rows[0].count); + + res.status(200).json({ + success: true, + users: usersResult.rows.map(user => ({ + id: user.id, + username: user.username, + email: user.email, + firstName: user.first_name, + lastName: user.last_name, + isActive: user.is_active, + roles: user.roles || [], + createdAt: user.created_at, + lastLogin: user.last_login + })), + pagination: { + page, + limit, + total, + pages: Math.ceil(total / limit) + } + }); + + } else if (req.method === 'PUT') { + // Update user (activate/deactivate, change roles) + const parsedUrl = url.parse(req.url, true); + const userId = parsedUrl.query.id; + + if (!userId) { + return res.status(400).json({ error: 'User ID required' }); + } + + // Parse request body + const { isActive, roles } = req.body; + + // Update user status + if (typeof isActive === 'boolean') { + await client.query( + 'UPDATE users SET is_active = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', + [isActive, userId] + ); + } + + // Update user roles + if (roles && Array.isArray(roles)) { + // Remove existing roles + await client.query('DELETE FROM user_roles WHERE user_id = $1', [userId]); + + // Add new roles + for (const roleName of roles) { + const roleResult = await client.query('SELECT id FROM roles WHERE name = $1', [roleName]); + if (roleResult.rows.length > 0) { + await client.query( + 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)', + [userId, roleResult.rows[0].id] + ); + } + } + } + + // Get updated user data + const updatedUserQuery = ` + SELECT + u.id, u.username, u.email, u.first_name, u.last_name, + u.is_active, u.created_at, u.last_login, + ARRAY_AGG(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) as roles + FROM users u + LEFT JOIN user_roles ur ON u.id = ur.user_id + LEFT JOIN roles r ON ur.role_id = r.id + WHERE u.id = $1 + GROUP BY u.id, u.username, u.email, u.first_name, u.last_name, u.is_active, u.created_at, u.last_login + `; + + const updatedUser = await client.query(updatedUserQuery, [userId]); + + res.status(200).json({ + success: true, + message: 'User updated successfully', + user: { + id: updatedUser.rows[0].id, + username: updatedUser.rows[0].username, + email: updatedUser.rows[0].email, + firstName: updatedUser.rows[0].first_name, + lastName: updatedUser.rows[0].last_name, + isActive: updatedUser.rows[0].is_active, + roles: updatedUser.rows[0].roles || [], + createdAt: updatedUser.rows[0].created_at, + lastLogin: updatedUser.rows[0].last_login + } + }); + + } else if (req.method === 'DELETE') { + // Delete user (soft delete - deactivate) + const parsedUrl = url.parse(req.url, true); + const userId = parsedUrl.query.id; + + if (!userId) { + return res.status(400).json({ error: 'User ID required' }); + } + + await client.query( + 'UPDATE users SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1', + [userId] + ); + + res.status(200).json({ + success: true, + message: 'User deactivated successfully' + }); + + } else { + res.status(405).json({ error: 'Method not allowed' }); + } + + } catch (error) { + console.error('Admin users API error:', error); + + if (error.message === 'No token provided' || error.message === 'Invalid token or insufficient permissions' || error.message === 'Admin access required') { + res.status(401).json({ error: error.message }); + } else { + res.status(500).json({ + error: 'Internal server error', + details: error.message + }); + } + } finally { + client.release(); + } +} \ No newline at end of file diff --git a/api/admin/users.ts b/api/admin/users.ts deleted file mode 100644 index 98c448d..0000000 --- a/api/admin/users.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { Pool } from 'pg'; -import jwt from 'jsonwebtoken'; - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, - ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, -}); - -// Middleware to verify admin access -async function verifyAdmin(req: NextRequest) { - const authHeader = req.headers.get('authorization'); - if (!authHeader?.startsWith('Bearer ')) { - throw new Error('No token provided'); - } - - const token = authHeader.substring(7); - const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production'; - - try { - const decoded = jwt.verify(token, jwtSecret) as any; - - // Check if user has admin role - if (!decoded.roles?.includes('admin')) { - throw new Error('Admin access required'); - } - - return decoded; - } catch (error) { - throw new Error('Invalid token or insufficient permissions'); - } -} - -export default async function handler(req: NextRequest) { - const client = await pool.connect(); - - try { - // Verify admin access - await verifyAdmin(req); - - if (req.method === 'GET') { - // Get all users with their roles - const { searchParams } = new URL(req.url); - const page = parseInt(searchParams.get('page') || '1'); - const limit = parseInt(searchParams.get('limit') || '20'); - const search = searchParams.get('search') || ''; - const offset = (page - 1) * limit; - - let whereClause = ''; - let queryParams: any[] = [limit, offset]; - - if (search) { - whereClause = 'WHERE u.username ILIKE $3 OR u.email ILIKE $3 OR u.first_name ILIKE $3 OR u.last_name ILIKE $3'; - queryParams.push(`%${search}%`); - } - - const usersResult = await client.query(` - SELECT - u.id, u.username, u.email, u.first_name, u.last_name, - u.is_active, u.email_verified, u.created_at, u.last_login, - array_agg(r.name) as roles - FROM users u - LEFT JOIN user_roles ur ON u.id = ur.user_id - LEFT JOIN roles r ON ur.role_id = r.id - ${whereClause} - GROUP BY u.id, u.username, u.email, u.first_name, u.last_name, - u.is_active, u.email_verified, u.created_at, u.last_login - ORDER BY u.created_at DESC - LIMIT $1 OFFSET $2 - `, queryParams); - - // Get total count for pagination - const countResult = await client.query(` - SELECT COUNT(DISTINCT u.id) as total - FROM users u - ${whereClause.replace('$3', search ? '$1' : '')} - `, search ? [`%${search}%`] : []); - - return new NextResponse(JSON.stringify({ - success: true, - users: usersResult.rows.map(user => ({ - ...user, - roles: user.roles.filter(Boolean) // Remove null values - })), - pagination: { - page, - limit, - total: parseInt(countResult.rows[0].total), - totalPages: Math.ceil(countResult.rows[0].total / limit) - } - }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - - } else if (req.method === 'PUT') { - // Update user (activate/deactivate, change roles) - const { searchParams } = new URL(req.url); - const userId = searchParams.get('id'); - - if (!userId) { - return new NextResponse(JSON.stringify({ error: 'User ID required' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - - // Parse request body - let body: { isActive?: boolean; roles?: string[] }; - try { - const bodyText = await req.text(); - body = JSON.parse(bodyText); - } catch (parseError) { - return new NextResponse(JSON.stringify({ - error: 'Invalid JSON in request body' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - - const { isActive, roles } = body; - - // Update user status - if (typeof isActive === 'boolean') { - await client.query( - 'UPDATE users SET is_active = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', - [isActive, userId] - ); - } - - // Update user roles - if (roles && Array.isArray(roles)) { - // Remove existing roles - await client.query('DELETE FROM user_roles WHERE user_id = $1', [userId]); - - // Add new roles - for (const roleName of roles) { - const roleResult = await client.query('SELECT id FROM roles WHERE name = $1', [roleName]); - if (roleResult.rows.length > 0) { - await client.query( - 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)', - [userId, roleResult.rows[0].id] - ); - } - } - } - - // Get updated user data - const updatedUser = await client.query(` - SELECT - u.id, u.username, u.email, u.first_name, u.last_name, - u.is_active, u.email_verified, u.created_at, u.last_login, - array_agg(r.name) as roles - FROM users u - LEFT JOIN user_roles ur ON u.id = ur.user_id - LEFT JOIN roles r ON ur.role_id = r.id - WHERE u.id = $1 - GROUP BY u.id, u.username, u.email, u.first_name, u.last_name, - u.is_active, u.email_verified, u.created_at, u.last_login - `, [userId]); - - return new NextResponse(JSON.stringify({ - success: true, - message: 'User updated successfully', - user: { - ...updatedUser.rows[0], - roles: updatedUser.rows[0].roles.filter(Boolean) - } - }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - - } else if (req.method === 'DELETE') { - // Delete user (soft delete by deactivating) - const { searchParams } = new URL(req.url); - const userId = searchParams.get('id'); - - if (!userId) { - return new NextResponse(JSON.stringify({ error: 'User ID required' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - - await client.query( - 'UPDATE users SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1', - [userId] - ); - - return new NextResponse(JSON.stringify({ - success: true, - message: 'User deactivated successfully' - }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - - } else { - return new NextResponse(JSON.stringify({ error: 'Method not allowed' }), { - status: 405, - headers: { 'Content-Type': 'application/json' }, - }); - } - - } catch (error) { - console.error('Admin users API error:', error); - - if ((error as Error).message.includes('Admin access required') || - (error as Error).message.includes('No token provided') || - (error as Error).message.includes('Invalid token')) { - return new NextResponse(JSON.stringify({ - error: 'Unauthorized', - message: (error as Error).message - }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - }); - } - - return new NextResponse(JSON.stringify({ - error: 'Internal server error', - details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined - }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }); - } finally { - client.release(); - } -} \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 633c0be..46b302c 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -137,9 +137,9 @@ const Navbar: React.FC = () => { setIsUserMenuOpen(false)} - className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + className="block px-4 py-2 text-sm text-purple-700 hover:bg-purple-50 font-medium" > - ⚡ Admin Panel + 👑 Admin Panel )}