Add 3-level role permissions, organismos operadores, and Histórico de Tomas page
Implements the full ADMIN → ORGANISMO_OPERADOR → OPERATOR permission hierarchy with scope-filtered data access across all backend services. Adds organismos operadores management (ADMIN only) and a new Histórico page for viewing per-meter reading history with chart, consumption stats, and CSV export. Key changes: - Backend: 3-level scope filtering on all services (meters, readings, projects, users) - Backend: Protect GET /meters routes with authenticateToken for role-based filtering - Backend: Pass requestingUser to reading service for scoped meter readings - Frontend: New HistoricoPage with meter selector, AreaChart, paginated table - Frontend: Consumption cards (Actual, Pasado, Diferencial) above date filters - Frontend: Meter search by name, serial, location, CESPT account, cadastral key - Frontend: OrganismosPage, updated Sidebar with 3-level visibility - SQL migrations for organismos_operadores table and FK columns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,8 @@ export interface UserProfile {
|
||||
role: string;
|
||||
avatarUrl?: string | null;
|
||||
projectId?: string | null;
|
||||
organismoOperadorId?: string | null;
|
||||
organismoName?: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ export async function login(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<LoginResult> {
|
||||
// Find user by email with role name
|
||||
// Find user by email with role name and organismo
|
||||
const userResult = await query<{
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -57,9 +59,10 @@ export async function login(
|
||||
avatar_url: string | null;
|
||||
role_name: string;
|
||||
project_id: string | null;
|
||||
organismo_operador_id: string | null;
|
||||
created_at: Date;
|
||||
}>(
|
||||
`SELECT u.id, u.email, u.name, u.password_hash, u.avatar_url, r.name as role_name, u.project_id, u.created_at
|
||||
`SELECT u.id, u.email, u.name, u.password_hash, u.avatar_url, r.name as role_name, u.project_id, u.organismo_operador_id, u.created_at
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE LOWER(u.email) = LOWER($1) AND u.is_active = true
|
||||
@@ -86,6 +89,7 @@ export async function login(
|
||||
roleId: user.id,
|
||||
roleName: user.role_name,
|
||||
projectId: user.project_id,
|
||||
organismoOperadorId: user.organismo_operador_id,
|
||||
});
|
||||
|
||||
const refreshToken = generateRefreshToken({
|
||||
@@ -174,8 +178,9 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
||||
email: string;
|
||||
role_name: string;
|
||||
project_id: string | null;
|
||||
organismo_operador_id: string | null;
|
||||
}>(
|
||||
`SELECT u.id, u.email, r.name as role_name, u.project_id
|
||||
`SELECT u.id, u.email, r.name as role_name, u.project_id, u.organismo_operador_id
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.id = $1 AND u.is_active = true
|
||||
@@ -195,6 +200,7 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
||||
roleId: user.id,
|
||||
roleName: user.role_name,
|
||||
projectId: user.project_id,
|
||||
organismoOperadorId: user.organismo_operador_id,
|
||||
});
|
||||
|
||||
return { accessToken };
|
||||
@@ -232,11 +238,15 @@ export async function getMe(userId: string): Promise<UserProfile> {
|
||||
avatar_url: string | null;
|
||||
role_name: string;
|
||||
project_id: string | null;
|
||||
organismo_operador_id: string | null;
|
||||
organismo_name: string | null;
|
||||
created_at: Date;
|
||||
}>(
|
||||
`SELECT u.id, u.email, u.name, u.avatar_url, r.name as role_name, u.project_id, u.created_at
|
||||
`SELECT u.id, u.email, u.name, u.avatar_url, r.name as role_name, u.project_id,
|
||||
u.organismo_operador_id, oo.name as organismo_name, u.created_at
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
LEFT JOIN organismos_operadores oo ON u.organismo_operador_id = oo.id
|
||||
WHERE u.id = $1 AND u.is_active = true
|
||||
LIMIT 1`,
|
||||
[userId]
|
||||
@@ -255,6 +265,8 @@ export async function getMe(userId: string): Promise<UserProfile> {
|
||||
role: user.role_name,
|
||||
avatarUrl: user.avatar_url,
|
||||
projectId: user.project_id,
|
||||
organismoOperadorId: user.organismo_operador_id,
|
||||
organismoName: user.organismo_name,
|
||||
createdAt: user.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user