From 46aab5fbba89a6e80afe9cb0c363fdcc4638b978 Mon Sep 17 00:00:00 2001 From: Esteban Date: Mon, 2 Feb 2026 01:27:15 -0600 Subject: [PATCH] Projects view by user --- src/api/users.ts | 3 +++ src/pages/UsersPage.tsx | 6 +++++- .../controllers/concentrator.controller.ts | 16 ++++++++++++---- water-api/src/controllers/meter.controller.ts | 10 ++++++++-- .../src/services/concentrator.service.ts | 14 ++++++++++++-- water-api/src/services/meter.service.ts | 16 ++++++++++++++-- water-api/src/services/user.service.ts | 19 ++++++++++++++++--- 7 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/api/users.ts b/src/api/users.ts index f2f1eab..b2a3d51 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -17,6 +17,7 @@ export interface User { description: string; permissions: Record>; }; + project_id: string | null; is_active: boolean; last_login: string | null; created_at: string; @@ -28,6 +29,7 @@ export interface CreateUserInput { password: string; name: string; role_id: string; + project_id?: string | null; is_active?: boolean; } @@ -35,6 +37,7 @@ export interface UpdateUserInput { email?: string; name?: string; role_id?: string; + project_id?: string | null; is_active?: boolean; } diff --git a/src/pages/UsersPage.tsx b/src/pages/UsersPage.tsx index 4430d52..3c8e631 100644 --- a/src/pages/UsersPage.tsx +++ b/src/pages/UsersPage.tsx @@ -16,6 +16,7 @@ interface User { email: string; roleId: string; roleName: string; + projectId: string | null; status: "ACTIVE" | "INACTIVE"; createdAt: string; } @@ -65,6 +66,7 @@ export default function UsersPage() { email: apiUser.email, roleId: apiUser.role_id, roleName: apiUser.role?.name || '', + projectId: apiUser.project_id || null, status: apiUser.is_active ? "ACTIVE" : "INACTIVE", createdAt: new Date(apiUser.created_at).toISOString().slice(0, 10) })); @@ -126,6 +128,7 @@ export default function UsersPage() { email: form.email, name: form.name.trim(), role_id: form.roleId, + project_id: form.projectId || null, is_active: form.status === "ACTIVE", }; @@ -136,6 +139,7 @@ export default function UsersPage() { password: form.password!, name: form.name.trim(), role_id: form.roleId, + project_id: form.projectId || null, is_active: form.status === "ACTIVE", }; @@ -221,7 +225,7 @@ export default function UsersPage() { name: user.name, email: user.email, roleId: user.roleId, - projectId: "", + projectId: user.projectId || "", status: user.status, createdAt: user.createdAt, password: "" diff --git a/water-api/src/controllers/concentrator.controller.ts b/water-api/src/controllers/concentrator.controller.ts index 8570d63..01fe205 100644 --- a/water-api/src/controllers/concentrator.controller.ts +++ b/water-api/src/controllers/concentrator.controller.ts @@ -1,4 +1,5 @@ -import { Request, Response } from 'express'; +import { Response } from 'express'; +import { AuthenticatedRequest } from '../types'; import * as concentratorService from '../services/concentrator.service'; import { CreateConcentratorInput, UpdateConcentratorInput } from '../validators/concentrator.validator'; @@ -7,13 +8,14 @@ import { CreateConcentratorInput, UpdateConcentratorInput } from '../validators/ * 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 { +export async function getAll(req: AuthenticatedRequest, res: Response): Promise { try { - const { project_id, status, page, limit, sortBy, sortOrder } = req.query; + const { project_id, status, type, 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; + if (type) filters.type = type as concentratorService.ConcentratorType; const pagination: concentratorService.PaginationOptions = { page: page ? parseInt(page as string, 10) : 1, @@ -22,7 +24,13 @@ export async function getAll(req: Request, res: Response): Promise { sortOrder: sortOrder as 'asc' | 'desc', }; - const result = await concentratorService.getAll(filters, pagination); + // Pass user info for role-based filtering + const requestingUser = req.user ? { + roleName: req.user.roleName, + projectId: req.user.projectId + } : undefined; + + const result = await concentratorService.getAll(filters, pagination, requestingUser); res.status(200).json({ success: true, diff --git a/water-api/src/controllers/meter.controller.ts b/water-api/src/controllers/meter.controller.ts index b7c0ac1..777648a 100644 --- a/water-api/src/controllers/meter.controller.ts +++ b/water-api/src/controllers/meter.controller.ts @@ -8,7 +8,7 @@ import * as readingService from '../services/reading.service'; * 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 { +export async function getAll(req: AuthenticatedRequest, res: Response): Promise { try { const page = parseInt(req.query.page as string, 10) || 1; const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 50, 1000); @@ -35,7 +35,13 @@ export async function getAll(req: Request, res: Response): Promise { filters.search = req.query.search as string; } - const result = await meterService.getAll(filters, { page, pageSize }); + // Pass user info for role-based filtering + const requestingUser = req.user ? { + roleName: req.user.roleName, + projectId: req.user.projectId + } : undefined; + + const result = await meterService.getAll(filters, { page, pageSize }, requestingUser); res.status(200).json({ success: true, diff --git a/water-api/src/services/concentrator.service.ts b/water-api/src/services/concentrator.service.ts index 9f3415b..dc77914 100644 --- a/water-api/src/services/concentrator.service.ts +++ b/water-api/src/services/concentrator.service.ts @@ -68,11 +68,13 @@ export interface PaginatedResult { * Get all concentrators with optional filters and pagination * @param filters - Optional filter criteria * @param pagination - Optional pagination options + * @param requestingUser - User making the request (for role-based filtering) * @returns Paginated list of concentrators */ export async function getAll( filters?: ConcentratorFilters, - pagination?: PaginationOptions + pagination?: PaginationOptions, + requestingUser?: { roleName: string; projectId?: string | null } ): Promise> { const page = pagination?.page || 1; const limit = pagination?.limit || 10; @@ -85,7 +87,15 @@ export async function getAll( const params: unknown[] = []; let paramIndex = 1; - if (filters?.project_id) { + // Role-based filtering: OPERATOR users can only see their assigned project + if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) { + conditions.push(`project_id = $${paramIndex}`); + params.push(requestingUser.projectId); + paramIndex++; + } + + // Additional filter by project_id (only applies if user is ADMIN or no user context) + if (filters?.project_id && (!requestingUser || requestingUser.roleName === 'ADMIN')) { conditions.push(`project_id = $${paramIndex}`); params.push(filters.project_id); paramIndex++; diff --git a/water-api/src/services/meter.service.ts b/water-api/src/services/meter.service.ts index 23172de..23ed9e8 100644 --- a/water-api/src/services/meter.service.ts +++ b/water-api/src/services/meter.service.ts @@ -92,10 +92,14 @@ export interface UpdateMeterInput { /** * Get all meters with optional filtering and pagination + * @param filters - Optional filter criteria + * @param pagination - Optional pagination options + * @param requestingUser - User making the request (for role-based filtering) */ export async function getAll( filters?: MeterFilters, - pagination?: PaginationParams + pagination?: PaginationParams, + requestingUser?: { roleName: string; projectId?: string | null } ): Promise> { const page = pagination?.page || 1; const pageSize = pagination?.pageSize || 50; @@ -105,13 +109,21 @@ export async function getAll( const params: unknown[] = []; let paramIndex = 1; + // Role-based filtering: OPERATOR users can only see meters from their assigned project + if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) { + conditions.push(`c.project_id = $${paramIndex}`); + params.push(requestingUser.projectId); + paramIndex++; + } + if (filters?.concentrator_id) { conditions.push(`m.concentrator_id = $${paramIndex}`); params.push(filters.concentrator_id); paramIndex++; } - if (filters?.project_id) { + // Additional filter by project_id (only applies if user is ADMIN or no user context) + if (filters?.project_id && (!requestingUser || requestingUser.roleName === 'ADMIN')) { conditions.push(`c.project_id = $${paramIndex}`); params.push(filters.project_id); paramIndex++; diff --git a/water-api/src/services/user.service.ts b/water-api/src/services/user.service.ts index 1b7cf2d..8840b17 100644 --- a/water-api/src/services/user.service.ts +++ b/water-api/src/services/user.service.ts @@ -93,6 +93,7 @@ export async function getAll( u.role_id, r.name as role_name, r.description as role_description, + u.project_id, u.is_active, u.last_login, u.created_at, @@ -122,6 +123,7 @@ export async function getAll( updated_at: row.updated_at, } : undefined, + project_id: row.project_id, is_active: row.is_active, last_login: row.last_login, created_at: row.created_at, @@ -160,6 +162,7 @@ export async function getById(id: string): Promise { r.name as role_name, r.description as role_description, r.permissions as role_permissions, + u.project_id, u.is_active, u.last_login, u.created_at, @@ -192,6 +195,7 @@ export async function getById(id: string): Promise { updated_at: row.updated_at, } : undefined, + project_id: row.project_id, is_active: row.is_active, last_login: row.last_login, created_at: row.created_at, @@ -235,6 +239,7 @@ export async function create(data: { name: string; avatar_url?: string | null; role_id: string; + project_id?: string | null; is_active?: boolean; }): Promise { // Check if email already exists @@ -248,9 +253,9 @@ export async function create(data: { const result = await query( ` - INSERT INTO users (email, password_hash, name, avatar_url, role_id, is_active) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id, email, name, avatar_url, role_id, is_active, last_login, created_at, updated_at + INSERT INTO users (email, password_hash, name, avatar_url, role_id, project_id, is_active) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, email, name, avatar_url, role_id, project_id, is_active, last_login, created_at, updated_at `, [ data.email.toLowerCase(), @@ -258,6 +263,7 @@ export async function create(data: { data.name, data.avatar_url ?? null, data.role_id, + data.project_id ?? null, data.is_active ?? true, ] ); @@ -281,6 +287,7 @@ export async function update( name?: string; avatar_url?: string | null; role_id?: string; + project_id?: string | null; is_active?: boolean; } ): Promise { @@ -327,6 +334,12 @@ export async function update( paramIndex++; } + if (data.project_id !== undefined) { + updates.push(`project_id = $${paramIndex}`); + params.push(data.project_id); + paramIndex++; + } + if (data.is_active !== undefined) { updates.push(`is_active = $${paramIndex}`); params.push(data.is_active);