Migrar backend a PostgreSQL + Node.js/Express con nuevas funcionalidades
Backend (water-api/): - Crear API REST completa con Express + TypeScript - Implementar autenticación JWT con refresh tokens - CRUD completo para: projects, concentrators, meters, gateways, devices, users, roles - Agregar validación con Zod para todas las entidades - Implementar webhooks para The Things Stack (LoRaWAN) - Agregar endpoint de lecturas con filtros y resumen de consumo - Implementar carga masiva de medidores via Excel (.xlsx) Frontend: - Crear cliente HTTP con manejo automático de JWT y refresh - Actualizar todas las APIs para usar nuevo backend - Agregar sistema de autenticación real (login, logout, me) - Agregar selector de tipo (LORA, LoRaWAN, Grandes) en concentradores y medidores - Agregar campo Meter ID en medidores - Crear modal de carga masiva para medidores - Agregar página de consumo con gráficas y filtros - Corregir carga de proyectos independiente de datos existentes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
0
water-api/src/controllers/.gitkeep
Normal file
0
water-api/src/controllers/.gitkeep
Normal file
138
water-api/src/controllers/auth.controller.ts
Normal file
138
water-api/src/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/auth.middleware';
|
||||
import * as authService from '../services/auth.service';
|
||||
import { LoginInput, RefreshInput } from '../validators/auth.validator';
|
||||
|
||||
/**
|
||||
* POST /auth/login
|
||||
* Authenticate user with email and password
|
||||
* Returns access token and refresh token
|
||||
*/
|
||||
export async function login(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { email, password } = req.body as LoginInput;
|
||||
|
||||
const result = await authService.login(email, password);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
user: result.user,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Login failed';
|
||||
|
||||
// Use 401 for authentication failures
|
||||
if (message === 'Invalid email or password') {
|
||||
res.status(401).json({ success: false, error: message });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/refresh
|
||||
* Generate new access token using refresh token
|
||||
* Returns new access token
|
||||
*/
|
||||
export async function refresh(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { refreshToken } = req.body as RefreshInput;
|
||||
|
||||
const result = await authService.refresh(refreshToken);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
accessToken: result.accessToken,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Token refresh failed';
|
||||
|
||||
// Use 401 for invalid/expired tokens
|
||||
if (
|
||||
message === 'Invalid refresh token' ||
|
||||
message === 'Refresh token not found or revoked' ||
|
||||
message === 'Refresh token expired'
|
||||
) {
|
||||
res.status(401).json({ success: false, error: message });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/logout
|
||||
* Invalidate the refresh token
|
||||
* Requires authentication
|
||||
*/
|
||||
export async function logout(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({ success: false, error: 'Authentication required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { refreshToken } = req.body as RefreshInput;
|
||||
|
||||
if (refreshToken) {
|
||||
await authService.logout(userId, refreshToken);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Logout successful',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /auth/me
|
||||
* Get authenticated user's profile
|
||||
* Requires authentication
|
||||
*/
|
||||
export async function getMe(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({ success: false, error: 'Authentication required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await authService.getMe(userId);
|
||||
|
||||
// Transform avatarUrl to avatar_url for frontend compatibility
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
name: profile.name,
|
||||
role: profile.role,
|
||||
avatar_url: profile.avatarUrl,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get profile';
|
||||
|
||||
if (message === 'User not found') {
|
||||
res.status(404).json({ success: false, error: message });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
82
water-api/src/controllers/bulk-upload.controller.ts
Normal file
82
water-api/src/controllers/bulk-upload.controller.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Request, Response } from 'express';
|
||||
import multer from 'multer';
|
||||
import { bulkUploadMeters, generateMeterTemplate } from '../services/bulk-upload.service';
|
||||
|
||||
// Configure multer for memory storage
|
||||
const storage = multer.memoryStorage();
|
||||
|
||||
export const upload = multer({
|
||||
storage,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB max
|
||||
},
|
||||
fileFilter: (_req, file, cb) => {
|
||||
// Accept Excel files only
|
||||
const allowedMimes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel', // .xls
|
||||
];
|
||||
|
||||
if (allowedMimes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Solo se permiten archivos Excel (.xlsx, .xls)'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/bulk-upload/meters
|
||||
* Upload Excel file with meters data
|
||||
*/
|
||||
export async function uploadMeters(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'No se proporcionó ningún archivo',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await bulkUploadMeters(req.file.buffer);
|
||||
|
||||
res.status(result.success ? 200 : 207).json({
|
||||
success: result.success,
|
||||
data: {
|
||||
totalRows: result.totalRows,
|
||||
inserted: result.inserted,
|
||||
failed: result.errors.length,
|
||||
errors: result.errors.slice(0, 50), // Limit errors in response
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
console.error('Error in bulk upload:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Error procesando la carga masiva',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/bulk-upload/meters/template
|
||||
* Download Excel template for meters
|
||||
*/
|
||||
export async function downloadMeterTemplate(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const buffer = generateMeterTemplate();
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=plantilla_medidores.xlsx');
|
||||
res.send(buffer);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
console.error('Error generating template:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Error generando la plantilla',
|
||||
});
|
||||
}
|
||||
}
|
||||
202
water-api/src/controllers/concentrator.controller.ts
Normal file
202
water-api/src/controllers/concentrator.controller.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as concentratorService from '../services/concentrator.service';
|
||||
import { CreateConcentratorInput, UpdateConcentratorInput } from '../validators/concentrator.validator';
|
||||
|
||||
/**
|
||||
* GET /concentrators
|
||||
* Get all concentrators with optional filters and pagination
|
||||
* Query params: project_id, status, page, limit, sortBy, sortOrder
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { project_id, status, page, limit, sortBy, sortOrder } = req.query;
|
||||
|
||||
const filters: concentratorService.ConcentratorFilters = {};
|
||||
if (project_id) filters.project_id = project_id as string;
|
||||
if (status) filters.status = status as string;
|
||||
|
||||
const pagination: concentratorService.PaginationOptions = {
|
||||
page: page ? parseInt(page as string, 10) : 1,
|
||||
limit: limit ? parseInt(limit as string, 10) : 10,
|
||||
sortBy: sortBy as string,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
};
|
||||
|
||||
const result = await concentratorService.getAll(filters, pagination);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching concentrators:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch concentrators',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /concentrators/:id
|
||||
* Get a single concentrator by ID with gateway count
|
||||
*/
|
||||
export async function getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const concentrator = await concentratorService.getById(id);
|
||||
|
||||
if (!concentrator) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Concentrator not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: concentrator,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching concentrator:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch concentrator',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /concentrators
|
||||
* Create a new concentrator
|
||||
*/
|
||||
export async function create(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = req.body as CreateConcentratorInput;
|
||||
|
||||
const concentrator = await concentratorService.create(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: concentrator,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating concentrator:', error);
|
||||
|
||||
// Check for unique constraint violation
|
||||
if (error instanceof Error && error.message.includes('duplicate')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A concentrator with this serial number already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for foreign key violation (invalid project_id)
|
||||
if (error instanceof Error && error.message.includes('foreign key')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project_id: Project does not exist',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create concentrator',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /concentrators/:id
|
||||
* Update an existing concentrator
|
||||
*/
|
||||
export async function update(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = req.body as UpdateConcentratorInput;
|
||||
|
||||
const concentrator = await concentratorService.update(id, data);
|
||||
|
||||
if (!concentrator) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Concentrator not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: concentrator,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating concentrator:', error);
|
||||
|
||||
if (error instanceof Error && error.message.includes('duplicate')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A concentrator with this serial number already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('foreign key')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project_id: Project does not exist',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update concentrator',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /concentrators/:id
|
||||
* Delete a concentrator
|
||||
*/
|
||||
export async function remove(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await concentratorService.remove(id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Concentrator not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { message: 'Concentrator deleted successfully' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting concentrator:', error);
|
||||
|
||||
// Check for dependency error
|
||||
if (error instanceof Error && error.message.includes('Cannot delete')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete concentrator',
|
||||
});
|
||||
}
|
||||
}
|
||||
225
water-api/src/controllers/device.controller.ts
Normal file
225
water-api/src/controllers/device.controller.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as deviceService from '../services/device.service';
|
||||
import { CreateDeviceInput, UpdateDeviceInput } from '../validators/device.validator';
|
||||
|
||||
/**
|
||||
* GET /devices
|
||||
* Get all devices with optional filters and pagination
|
||||
* Query params: project_id, gateway_id, status, device_type, page, limit, sortBy, sortOrder
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { project_id, gateway_id, status, device_type, page, limit, sortBy, sortOrder } = req.query;
|
||||
|
||||
const filters: deviceService.DeviceFilters = {};
|
||||
if (project_id) filters.project_id = project_id as string;
|
||||
if (gateway_id) filters.gateway_id = gateway_id as string;
|
||||
if (status) filters.status = status as string;
|
||||
if (device_type) filters.device_type = device_type as string;
|
||||
|
||||
const pagination: deviceService.PaginationOptions = {
|
||||
page: page ? parseInt(page as string, 10) : 1,
|
||||
limit: limit ? parseInt(limit as string, 10) : 10,
|
||||
sortBy: sortBy as string,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
};
|
||||
|
||||
const result = await deviceService.getAll(filters, pagination);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching devices:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch devices',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /devices/:id
|
||||
* Get a single device by ID with meter info
|
||||
*/
|
||||
export async function getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const device = await deviceService.getById(id);
|
||||
|
||||
if (!device) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Device not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: device,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching device:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch device',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /devices/dev-eui/:devEui
|
||||
* Get a device by DevEUI
|
||||
*/
|
||||
export async function getByDevEui(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { devEui } = req.params;
|
||||
|
||||
const device = await deviceService.getByDevEui(devEui);
|
||||
|
||||
if (!device) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Device not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: device,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching device by DevEUI:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch device',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /devices
|
||||
* Create a new device
|
||||
*/
|
||||
export async function create(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = req.body as CreateDeviceInput;
|
||||
|
||||
const device = await deviceService.create(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: device,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating device:', error);
|
||||
|
||||
// Check for unique constraint violation
|
||||
if (error instanceof Error && error.message.includes('duplicate')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A device with this DevEUI already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for foreign key violation
|
||||
if (error instanceof Error && error.message.includes('foreign key')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project_id or gateway_id: Reference does not exist',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create device',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /devices/:id
|
||||
* Update an existing device
|
||||
*/
|
||||
export async function update(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = req.body as UpdateDeviceInput;
|
||||
|
||||
const device = await deviceService.update(id, data);
|
||||
|
||||
if (!device) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Device not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: device,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating device:', error);
|
||||
|
||||
if (error instanceof Error && error.message.includes('duplicate')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A device with this DevEUI already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('foreign key')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project_id or gateway_id: Reference does not exist',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update device',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /devices/:id
|
||||
* Delete a device
|
||||
*/
|
||||
export async function remove(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await deviceService.remove(id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Device not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { message: 'Device deleted successfully' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting device:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete device',
|
||||
});
|
||||
}
|
||||
}
|
||||
237
water-api/src/controllers/gateway.controller.ts
Normal file
237
water-api/src/controllers/gateway.controller.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as gatewayService from '../services/gateway.service';
|
||||
import { CreateGatewayInput, UpdateGatewayInput } from '../validators/gateway.validator';
|
||||
|
||||
/**
|
||||
* GET /gateways
|
||||
* Get all gateways with optional filters and pagination
|
||||
* Query params: project_id, concentrator_id, status, page, limit, sortBy, sortOrder
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { project_id, concentrator_id, status, page, limit, sortBy, sortOrder } = req.query;
|
||||
|
||||
const filters: gatewayService.GatewayFilters = {};
|
||||
if (project_id) filters.project_id = project_id as string;
|
||||
if (concentrator_id) filters.concentrator_id = concentrator_id as string;
|
||||
if (status) filters.status = status as string;
|
||||
|
||||
const pagination: gatewayService.PaginationOptions = {
|
||||
page: page ? parseInt(page as string, 10) : 1,
|
||||
limit: limit ? parseInt(limit as string, 10) : 10,
|
||||
sortBy: sortBy as string,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
};
|
||||
|
||||
const result = await gatewayService.getAll(filters, pagination);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching gateways:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch gateways',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /gateways/:id
|
||||
* Get a single gateway by ID with device count
|
||||
*/
|
||||
export async function getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const gateway = await gatewayService.getById(id);
|
||||
|
||||
if (!gateway) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Gateway not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: gateway,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching gateway:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch gateway',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /gateways/:id/devices
|
||||
* Get all devices for a specific gateway
|
||||
*/
|
||||
export async function getDevices(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// First check if gateway exists
|
||||
const gateway = await gatewayService.getById(id);
|
||||
|
||||
if (!gateway) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Gateway not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const devices = await gatewayService.getDevices(id);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: devices,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching gateway devices:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch gateway devices',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /gateways
|
||||
* Create a new gateway
|
||||
*/
|
||||
export async function create(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = req.body as CreateGatewayInput;
|
||||
|
||||
const gateway = await gatewayService.create(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: gateway,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating gateway:', error);
|
||||
|
||||
// Check for unique constraint violation
|
||||
if (error instanceof Error && error.message.includes('duplicate')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A gateway with this gateway_id already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for foreign key violation
|
||||
if (error instanceof Error && error.message.includes('foreign key')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project_id or concentrator_id: Reference does not exist',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create gateway',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /gateways/:id
|
||||
* Update an existing gateway
|
||||
*/
|
||||
export async function update(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = req.body as UpdateGatewayInput;
|
||||
|
||||
const gateway = await gatewayService.update(id, data);
|
||||
|
||||
if (!gateway) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Gateway not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: gateway,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating gateway:', error);
|
||||
|
||||
if (error instanceof Error && error.message.includes('duplicate')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A gateway with this gateway_id already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('foreign key')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project_id or concentrator_id: Reference does not exist',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update gateway',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /gateways/:id
|
||||
* Delete a gateway
|
||||
*/
|
||||
export async function remove(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await gatewayService.remove(id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Gateway not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { message: 'Gateway deleted successfully' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting gateway:', error);
|
||||
|
||||
// Check for dependency error
|
||||
if (error instanceof Error && error.message.includes('Cannot delete')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete gateway',
|
||||
});
|
||||
}
|
||||
}
|
||||
284
water-api/src/controllers/meter.controller.ts
Normal file
284
water-api/src/controllers/meter.controller.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/auth.middleware';
|
||||
import * as meterService from '../services/meter.service';
|
||||
import * as readingService from '../services/reading.service';
|
||||
|
||||
/**
|
||||
* GET /meters
|
||||
* List all meters with pagination and optional filtering
|
||||
* Query params: page, pageSize, concentrator_id, project_id, status, type, search
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string, 10) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 10, 100);
|
||||
|
||||
const filters: meterService.MeterFilters = {};
|
||||
|
||||
if (req.query.concentrator_id) {
|
||||
filters.concentrator_id = req.query.concentrator_id as string;
|
||||
}
|
||||
|
||||
if (req.query.project_id) {
|
||||
filters.project_id = req.query.project_id as string;
|
||||
}
|
||||
|
||||
if (req.query.status) {
|
||||
filters.status = req.query.status as string;
|
||||
}
|
||||
|
||||
if (req.query.type) {
|
||||
filters.type = req.query.type as string;
|
||||
}
|
||||
|
||||
if (req.query.search) {
|
||||
filters.search = req.query.search as string;
|
||||
}
|
||||
|
||||
const result = await meterService.getAll(filters, { page, pageSize });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching meters:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch meters',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /meters/:id
|
||||
* Get a single meter by ID with device info
|
||||
*/
|
||||
export async function getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const meter = await meterService.getById(id);
|
||||
|
||||
if (!meter) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Meter not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: meter,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching meter:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch meter',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /meters
|
||||
* Create a new meter
|
||||
* Requires authentication
|
||||
*/
|
||||
export async function create(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: 'Authentication required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = req.body as meterService.CreateMeterInput;
|
||||
|
||||
const meter = await meterService.create(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: meter,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create meter';
|
||||
|
||||
// Handle unique constraint violation
|
||||
if (message.includes('duplicate') || message.includes('unique')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A meter with this serial number already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle foreign key constraint violation
|
||||
if (message.includes('foreign key') || message.includes('violates')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid concentrator_id reference',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Error creating meter:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create meter',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /meters/:id
|
||||
* Update an existing meter
|
||||
* Requires authentication
|
||||
*/
|
||||
export async function update(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = req.body as meterService.UpdateMeterInput;
|
||||
|
||||
const meter = await meterService.update(id, data);
|
||||
|
||||
if (!meter) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Meter not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: meter,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update meter';
|
||||
|
||||
// Handle unique constraint violation
|
||||
if (message.includes('duplicate') || message.includes('unique')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'A meter with this serial number already exists',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle foreign key constraint violation
|
||||
if (message.includes('foreign key') || message.includes('violates')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid concentrator_id reference',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Error updating meter:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update meter',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /meters/:id
|
||||
* Delete a meter
|
||||
* Requires admin role
|
||||
*/
|
||||
export async function deleteMeter(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// First check if meter exists
|
||||
const meter = await meterService.getById(id);
|
||||
|
||||
if (!meter) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Meter not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await meterService.deleteMeter(id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete meter',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { message: 'Meter deleted successfully' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting meter:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete meter',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /meters/:id/readings
|
||||
* Get meter readings history with optional date range filter
|
||||
* Query params: start_date, end_date, page, pageSize
|
||||
*/
|
||||
export async function getReadings(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// First check if meter exists
|
||||
const meter = await meterService.getById(id);
|
||||
|
||||
if (!meter) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Meter not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const page = parseInt(req.query.page as string, 10) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 50, 100);
|
||||
|
||||
const filters: readingService.ReadingFilters = {
|
||||
meter_id: id,
|
||||
};
|
||||
|
||||
if (req.query.start_date) {
|
||||
filters.start_date = req.query.start_date as string;
|
||||
}
|
||||
|
||||
if (req.query.end_date) {
|
||||
filters.end_date = req.query.end_date as string;
|
||||
}
|
||||
|
||||
const result = await readingService.getAll(filters, { page, pageSize });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching meter readings:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch meter readings',
|
||||
});
|
||||
}
|
||||
}
|
||||
227
water-api/src/controllers/project.controller.ts
Normal file
227
water-api/src/controllers/project.controller.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/auth.middleware';
|
||||
import * as projectService from '../services/project.service';
|
||||
import { CreateProjectInput, UpdateProjectInput, ProjectStatusType } from '../validators/project.validator';
|
||||
|
||||
/**
|
||||
* GET /projects
|
||||
* List all projects with pagination and optional filtering
|
||||
* Query params: page, pageSize, status, area_name, search
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string, 10) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 10, 100);
|
||||
|
||||
const filters: projectService.ProjectFilters = {};
|
||||
|
||||
if (req.query.status) {
|
||||
filters.status = req.query.status as ProjectStatusType;
|
||||
}
|
||||
|
||||
if (req.query.area_name) {
|
||||
filters.area_name = req.query.area_name as string;
|
||||
}
|
||||
|
||||
if (req.query.search) {
|
||||
filters.search = req.query.search as string;
|
||||
}
|
||||
|
||||
const result = await projectService.getAll(filters, { page, pageSize });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching projects:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch projects',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /projects/:id
|
||||
* Get a single project by ID
|
||||
*/
|
||||
export async function getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const project = await projectService.getById(id);
|
||||
|
||||
if (!project) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Project not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: project,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching project:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch project',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /projects
|
||||
* Create a new project
|
||||
* Requires authentication
|
||||
*/
|
||||
export async function create(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: 'Authentication required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = req.body as CreateProjectInput;
|
||||
|
||||
const project = await projectService.create(data, userId);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: project,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create project',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /projects/:id
|
||||
* Update an existing project
|
||||
* Requires authentication
|
||||
*/
|
||||
export async function update(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = req.body as UpdateProjectInput;
|
||||
|
||||
const project = await projectService.update(id, data);
|
||||
|
||||
if (!project) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Project not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: project,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating project:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update project',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /projects/:id
|
||||
* Delete a project
|
||||
* Requires admin role
|
||||
*/
|
||||
export async function deleteProject(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// First check if project exists
|
||||
const project = await projectService.getById(id);
|
||||
|
||||
if (!project) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Project not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await projectService.deleteProject(id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete project',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { message: 'Project deleted successfully' },
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete project';
|
||||
|
||||
// Handle dependency error
|
||||
if (message.includes('Cannot delete project')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Error deleting project:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete project',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /projects/:id/stats
|
||||
* Get project statistics
|
||||
*/
|
||||
export async function getStats(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const stats = await projectService.getStats(id);
|
||||
|
||||
if (!stats) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Project not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: stats,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching project stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch project statistics',
|
||||
});
|
||||
}
|
||||
}
|
||||
158
water-api/src/controllers/reading.controller.ts
Normal file
158
water-api/src/controllers/reading.controller.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as readingService from '../services/reading.service';
|
||||
|
||||
/**
|
||||
* GET /readings
|
||||
* List all readings with pagination and filtering
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
page = '1',
|
||||
pageSize = '50',
|
||||
meter_id,
|
||||
concentrator_id,
|
||||
project_id,
|
||||
start_date,
|
||||
end_date,
|
||||
reading_type,
|
||||
} = req.query;
|
||||
|
||||
const filters: readingService.ReadingFilters = {};
|
||||
if (meter_id) filters.meter_id = meter_id as string;
|
||||
if (concentrator_id) filters.concentrator_id = concentrator_id as string;
|
||||
if (project_id) filters.project_id = project_id as string;
|
||||
if (start_date) filters.start_date = start_date as string;
|
||||
if (end_date) filters.end_date = end_date as string;
|
||||
if (reading_type) filters.reading_type = reading_type as string;
|
||||
|
||||
const pagination = {
|
||||
page: parseInt(page as string, 10),
|
||||
pageSize: Math.min(parseInt(pageSize as string, 10), 100), // Max 100 per page
|
||||
};
|
||||
|
||||
const result = await readingService.getAll(filters, pagination);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching readings:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /readings/:id
|
||||
* Get a single reading by ID
|
||||
*/
|
||||
export async function getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const reading = await readingService.getById(id);
|
||||
|
||||
if (!reading) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Reading not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: reading,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching reading:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /readings
|
||||
* Create a new reading
|
||||
*/
|
||||
export async function create(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = req.body as readingService.CreateReadingInput;
|
||||
|
||||
const reading = await readingService.create(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: reading,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating reading:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /readings/:id
|
||||
* Delete a reading
|
||||
*/
|
||||
export async function deleteReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await readingService.deleteReading(id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Reading not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { message: 'Reading deleted successfully' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting reading:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /readings/summary
|
||||
* Get consumption summary statistics
|
||||
*/
|
||||
export async function getSummary(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { project_id } = req.query;
|
||||
|
||||
const summary = await readingService.getConsumptionSummary(
|
||||
project_id as string | undefined
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: summary,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching summary:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
222
water-api/src/controllers/role.controller.ts
Normal file
222
water-api/src/controllers/role.controller.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/auth.middleware';
|
||||
import * as roleService from '../services/role.service';
|
||||
import { CreateRoleInput, UpdateRoleInput } from '../validators/role.validator';
|
||||
|
||||
/**
|
||||
* GET /roles
|
||||
* List all roles (all authenticated users)
|
||||
*/
|
||||
export async function getAllRoles(
|
||||
_req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const roles = await roleService.getAll();
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Roles retrieved successfully',
|
||||
data: roles,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to retrieve roles';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /roles/:id
|
||||
* Get a single role by ID with user count (all authenticated users)
|
||||
*/
|
||||
export async function getRoleById(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const roleId = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(roleId)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid role ID',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const role = await roleService.getById(roleId);
|
||||
|
||||
if (!role) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Role not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Role retrieved successfully',
|
||||
data: role,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to retrieve role';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /roles
|
||||
* Create a new role (admin only)
|
||||
*/
|
||||
export async function createRole(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = req.body as CreateRoleInput;
|
||||
|
||||
const role = await roleService.create({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
permissions: data.permissions as Record<string, unknown> | undefined,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Role created successfully',
|
||||
data: role,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create role';
|
||||
|
||||
if (message === 'Role name already exists') {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /roles/:id
|
||||
* Update a role (admin only)
|
||||
*/
|
||||
export async function updateRole(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const roleId = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(roleId)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid role ID',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = req.body as UpdateRoleInput;
|
||||
|
||||
const role = await roleService.update(roleId, {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
permissions: data.permissions as Record<string, unknown> | undefined,
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Role not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Role updated successfully',
|
||||
data: role,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update role';
|
||||
|
||||
if (message === 'Role name already exists') {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /roles/:id
|
||||
* Delete a role (admin only, only if no users assigned)
|
||||
*/
|
||||
export async function deleteRole(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const roleId = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(roleId)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid role ID',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await roleService.deleteRole(roleId);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Role not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Role deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete role';
|
||||
|
||||
// Handle case where users are assigned to the role
|
||||
if (message.includes('Cannot delete role')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
194
water-api/src/controllers/tts.controller.ts
Normal file
194
water-api/src/controllers/tts.controller.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { Request, Response } from 'express';
|
||||
import logger from '../utils/logger';
|
||||
import * as ttsWebhookService from '../services/tts/ttsWebhook.service';
|
||||
import {
|
||||
TtsUplinkPayload,
|
||||
TtsJoinPayload,
|
||||
TtsDownlinkAckPayload,
|
||||
} from '../validators/tts.validator';
|
||||
|
||||
/**
|
||||
* Extended request interface for TTS webhooks
|
||||
*/
|
||||
export interface TtsWebhookRequest extends Request {
|
||||
ttsVerified?: boolean;
|
||||
ttsApiKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/webhooks/tts/uplink
|
||||
* Handle uplink webhook from The Things Stack
|
||||
*
|
||||
* This endpoint receives uplink messages when devices send data.
|
||||
* The payload is validated, logged, decoded, and used to create meter readings.
|
||||
*/
|
||||
export async function handleUplink(req: TtsWebhookRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const payload = req.body as TtsUplinkPayload;
|
||||
|
||||
logger.info('Received TTS uplink webhook', {
|
||||
devEui: payload.end_device_ids.dev_eui,
|
||||
deviceId: payload.end_device_ids.device_id,
|
||||
fPort: payload.uplink_message.f_port,
|
||||
verified: req.ttsVerified,
|
||||
});
|
||||
|
||||
const result = await ttsWebhookService.processUplink(payload);
|
||||
|
||||
if (result.success) {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Uplink processed successfully',
|
||||
data: {
|
||||
logId: result.logId,
|
||||
deviceId: result.deviceId,
|
||||
meterId: result.meterId,
|
||||
readingId: result.readingId,
|
||||
readingValue: result.decodedPayload?.readingValue,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// We still return 200 to TTS to prevent retries for known issues
|
||||
// (device not found, decoding failed, etc.)
|
||||
res.status(200).json({
|
||||
success: false,
|
||||
message: result.error || 'Failed to process uplink',
|
||||
data: {
|
||||
logId: result.logId,
|
||||
deviceId: result.deviceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Error handling TTS uplink webhook', { error: errorMessage });
|
||||
|
||||
// Return 500 to trigger TTS retry mechanism for unexpected errors
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/webhooks/tts/join
|
||||
* Handle join webhook from The Things Stack
|
||||
*
|
||||
* This endpoint receives join accept messages when devices join the network.
|
||||
* Updates device status to 'JOINED'.
|
||||
*/
|
||||
export async function handleJoin(req: TtsWebhookRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const payload = req.body as TtsJoinPayload;
|
||||
|
||||
logger.info('Received TTS join webhook', {
|
||||
devEui: payload.end_device_ids.dev_eui,
|
||||
deviceId: payload.end_device_ids.device_id,
|
||||
verified: req.ttsVerified,
|
||||
});
|
||||
|
||||
const result = await ttsWebhookService.processJoin(payload);
|
||||
|
||||
if (result.success) {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Join event processed successfully',
|
||||
data: {
|
||||
deviceId: result.deviceId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Return 200 even on failure to prevent unnecessary retries
|
||||
res.status(200).json({
|
||||
success: false,
|
||||
message: result.error || 'Failed to process join event',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Error handling TTS join webhook', { error: errorMessage });
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/webhooks/tts/downlink/ack
|
||||
* Handle downlink acknowledgment webhook from The Things Stack
|
||||
*
|
||||
* This endpoint receives confirmations when downlink messages are
|
||||
* acknowledged by devices, sent, failed, or queued.
|
||||
*/
|
||||
export async function handleDownlinkAck(req: TtsWebhookRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const payload = req.body as TtsDownlinkAckPayload;
|
||||
|
||||
// Determine event type for logging
|
||||
let eventType = 'ack';
|
||||
if (payload.downlink_sent) eventType = 'sent';
|
||||
if (payload.downlink_failed) eventType = 'failed';
|
||||
if (payload.downlink_queued) eventType = 'queued';
|
||||
|
||||
logger.info('Received TTS downlink webhook', {
|
||||
devEui: payload.end_device_ids.dev_eui,
|
||||
deviceId: payload.end_device_ids.device_id,
|
||||
eventType,
|
||||
verified: req.ttsVerified,
|
||||
});
|
||||
|
||||
const result = await ttsWebhookService.processDownlinkAck(payload);
|
||||
|
||||
if (result.success) {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Downlink event processed successfully',
|
||||
data: {
|
||||
logId: result.logId,
|
||||
deviceId: result.deviceId,
|
||||
eventType,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
res.status(200).json({
|
||||
success: false,
|
||||
message: result.error || 'Failed to process downlink event',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Error handling TTS downlink webhook', { error: errorMessage });
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/webhooks/tts/health
|
||||
* Health check endpoint for TTS webhooks
|
||||
*
|
||||
* Can be used by TTS or monitoring systems to verify the webhook endpoint is available.
|
||||
*/
|
||||
export async function healthCheck(_req: Request, res: Response): Promise<void> {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'TTS webhook endpoint is healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
handleUplink,
|
||||
handleJoin,
|
||||
handleDownlinkAck,
|
||||
healthCheck,
|
||||
};
|
||||
352
water-api/src/controllers/user.controller.ts
Normal file
352
water-api/src/controllers/user.controller.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
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 = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(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.toString();
|
||||
|
||||
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,
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
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 = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(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.toString();
|
||||
|
||||
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 = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(userId)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid user ID',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent admin from deleting themselves
|
||||
if (req.user?.id === userId.toString()) {
|
||||
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 = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(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.toString()) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user