Files
GRH/water-api/src/controllers/user.controller.ts
Esteban 2d977b13b4 Convert user and role IDs from number to UUID string
Updated backend and frontend to use UUID strings instead of integers for user and role IDs
to match PostgreSQL database schema (UUID type).

Backend changes:
- Updated User and UserPublic interfaces: id and role_id now string (UUID)
- Updated JwtPayload: userId and roleId now string
- Updated user validators: role_id validation changed from number to UUID string
- Removed parseInt() calls in user controller (getUserById, updateUser, deleteUser, changePassword)
- Updated all user service function signatures to accept string IDs
- Updated create() and update() functions to accept role_id as string

Frontend changes:
- Updated User interface in users API: role_id is string
- Updated CreateUserInput and UpdateUserInput: role_id is string
- Added role filter in UsersPage sidebar
- Removed number conversion logic (parseInt)

This fixes the "invalid input syntax for type uuid" error when creating/updating users.
2026-01-26 19:49:15 -06:00

353 lines
7.8 KiB
TypeScript

import { Response } from 'express';
import { AuthenticatedRequest } from '../middleware/auth.middleware';
import * as userService from '../services/user.service';
import {
CreateUserInput,
UpdateUserInput,
ChangePasswordInput,
} from '../validators/user.validator';
/**
* GET /users
* List all users (admin only)
* Supports filtering by role_id, is_active, and search
* Supports pagination with page, limit, sortBy, sortOrder
*/
export async function getAllUsers(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
// Parse query parameters for filters
const filters: userService.UserFilter = {};
if (req.query.role_id) {
filters.role_id = parseInt(req.query.role_id as string, 10);
}
if (req.query.is_active !== undefined) {
filters.is_active = req.query.is_active === 'true';
}
if (req.query.search) {
filters.search = req.query.search as string;
}
// Parse pagination parameters
const pagination = {
page: parseInt(req.query.page as string, 10) || 1,
limit: parseInt(req.query.limit as string, 10) || 10,
sortBy: (req.query.sortBy as string) || 'created_at',
sortOrder: (req.query.sortOrder as 'asc' | 'desc') || 'desc',
};
const result = await userService.getAll(filters, pagination);
res.status(200).json({
success: true,
message: 'Users retrieved successfully',
data: result.users,
pagination: result.pagination,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to retrieve users';
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* GET /users/:id
* Get a single user by ID (admin or self)
*/
export async function getUserById(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.params.id;
if (!userId) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
// Check if user is admin or requesting their own data
const requestingUser = req.user;
const isAdmin = requestingUser?.role === 'ADMIN';
const isSelf = requestingUser?.id === userId;
if (!isAdmin && !isSelf) {
res.status(403).json({
success: false,
error: 'Insufficient permissions',
});
return;
}
const user = await userService.getById(userId);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found',
});
return;
}
res.status(200).json({
success: true,
message: 'User retrieved successfully',
data: user,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to retrieve user';
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* POST /users
* Create a new user (admin only)
*/
export async function createUser(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const data = req.body as CreateUserInput;
const user = await userService.create({
email: data.email,
password: data.password,
name: data.name,
avatar_url: data.avatar_url,
role_id: data.role_id,
is_active: data.is_active,
});
res.status(201).json({
success: true,
message: 'User created successfully',
data: user,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to create user';
if (message === 'Email already in use') {
res.status(409).json({
success: false,
error: message,
});
return;
}
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* PUT /users/:id
* Update a user (admin can update all fields, regular users can only update limited fields on self)
*/
export async function updateUser(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.params.id;
if (!userId) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
const requestingUser = req.user;
const isAdmin = requestingUser?.role === 'ADMIN';
const isSelf = requestingUser?.id === userId;
if (!isAdmin && !isSelf) {
res.status(403).json({
success: false,
error: 'Insufficient permissions',
});
return;
}
const data = req.body as UpdateUserInput;
// Non-admin users can only update their own profile fields (not role_id or is_active)
if (!isAdmin) {
if (data.role_id !== undefined || data.is_active !== undefined) {
res.status(403).json({
success: false,
error: 'You can only update your profile information',
});
return;
}
}
const user = await userService.update(userId, data);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found',
});
return;
}
res.status(200).json({
success: true,
message: 'User updated successfully',
data: user,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to update user';
if (message === 'Email already in use') {
res.status(409).json({
success: false,
error: message,
});
return;
}
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* DELETE /users/:id
* Deactivate a user (soft delete, admin only)
*/
export async function deleteUser(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.params.id;
if (!userId) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
// Prevent admin from deleting themselves
if (req.user?.id === userId) {
res.status(400).json({
success: false,
error: 'Cannot deactivate your own account',
});
return;
}
const deleted = await userService.deleteUser(userId);
if (!deleted) {
res.status(404).json({
success: false,
error: 'User not found',
});
return;
}
res.status(200).json({
success: true,
message: 'User deactivated successfully',
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to deactivate user';
res.status(500).json({
success: false,
error: message,
});
}
}
/**
* PUT /users/:id/password
* Change user password (self only)
*/
export async function changePassword(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.params.id;
if (!userId) {
res.status(400).json({
success: false,
error: 'Invalid user ID',
});
return;
}
// Only allow users to change their own password
if (req.user?.id !== userId) {
res.status(403).json({
success: false,
error: 'You can only change your own password',
});
return;
}
const data = req.body as ChangePasswordInput;
await userService.changePassword(
userId,
data.current_password,
data.new_password
);
res.status(200).json({
success: true,
message: 'Password changed successfully',
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to change password';
if (message === 'Current password is incorrect') {
res.status(401).json({
success: false,
error: message,
});
return;
}
if (message === 'User not found') {
res.status(404).json({
success: false,
error: message,
});
return;
}
res.status(500).json({
success: false,
error: message,
});
}
}