Fix admin users API endpoint
🔧 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
This commit is contained in:
parent
04d7383025
commit
08e2664a9a
3 changed files with 216 additions and 234 deletions
214
api/admin/users.js
Normal file
214
api/admin/users.js
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -137,9 +137,9 @@ const Navbar: React.FC = () => {
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => 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
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue