From 1d278936b1675653146fa919f23ccfe8b3c37686 Mon Sep 17 00:00:00 2001 From: Esteban Date: Mon, 2 Feb 2026 01:14:57 -0600 Subject: [PATCH] add user-project relation and role-based filtering --- water-api/sql/add_user_project_relation.sql | 27 +++++++++++++++++++++ water-api/src/middleware/auth.middleware.ts | 1 + water-api/src/services/auth.service.ts | 8 ++++-- water-api/src/types/index.ts | 3 +++ water-api/src/validators/user.validator.ts | 10 ++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 water-api/sql/add_user_project_relation.sql diff --git a/water-api/sql/add_user_project_relation.sql b/water-api/sql/add_user_project_relation.sql new file mode 100644 index 0000000..8d744e3 --- /dev/null +++ b/water-api/sql/add_user_project_relation.sql @@ -0,0 +1,27 @@ +-- ============================================================================ +-- Add project_id to users table +-- This allows assigning a specific project to OPERATOR users +-- ADMIN users don't need a project assignment (can see all projects) +-- ============================================================================ + +-- Add project_id column to users table +ALTER TABLE users +ADD COLUMN project_id UUID REFERENCES projects(id) ON DELETE SET NULL; + +-- Add index for better query performance +CREATE INDEX idx_users_project_id ON users(project_id); + +-- Add comment +COMMENT ON COLUMN users.project_id IS 'Assigned project for OPERATOR users. NULL for ADMIN users who can access all projects.'; + +-- ============================================================================ +-- Verify the change +-- ============================================================================ +SELECT + column_name, + data_type, + is_nullable, + column_default +FROM information_schema.columns +WHERE table_name = 'users' + AND column_name = 'project_id'; diff --git a/water-api/src/middleware/auth.middleware.ts b/water-api/src/middleware/auth.middleware.ts index 1e07d5f..820944e 100644 --- a/water-api/src/middleware/auth.middleware.ts +++ b/water-api/src/middleware/auth.middleware.ts @@ -41,6 +41,7 @@ export function authenticateToken( email: (decoded as any).email, roleId: (decoded as any).roleId || (decoded as any).role, roleName: (decoded as any).roleName || (decoded as any).role, + projectId: (decoded as any).projectId, }; next(); diff --git a/water-api/src/services/auth.service.ts b/water-api/src/services/auth.service.ts index 5d22a11..a2dc752 100644 --- a/water-api/src/services/auth.service.ts +++ b/water-api/src/services/auth.service.ts @@ -55,9 +55,10 @@ export async function login( password_hash: string; avatar_url: string | null; role_name: string; + project_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.created_at + `SELECT u.id, u.email, u.name, u.password_hash, u.avatar_url, r.name as role_name, u.project_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 @@ -83,6 +84,7 @@ export async function login( email: user.email, roleId: user.id, roleName: user.role_name, + projectId: user.project_id, }); const refreshToken = generateRefreshToken({ @@ -170,8 +172,9 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri id: string; email: string; role_name: string; + project_id: string | null; }>( - `SELECT u.id, u.email, r.name as role_name + `SELECT u.id, u.email, r.name as role_name, u.project_id FROM users u JOIN roles r ON u.role_id = r.id WHERE u.id = $1 AND u.is_active = true @@ -190,6 +193,7 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri email: user.email, roleId: user.id, roleName: user.role_name, + projectId: user.project_id, }); return { accessToken }; diff --git a/water-api/src/types/index.ts b/water-api/src/types/index.ts index 234ce88..99b956e 100644 --- a/water-api/src/types/index.ts +++ b/water-api/src/types/index.ts @@ -30,6 +30,7 @@ export interface User { avatar_url: string | null; role_id: string; role?: Role; + project_id: string | null; is_active: boolean; last_login: Date | null; created_at: Date; @@ -43,6 +44,7 @@ export interface UserPublic { avatar_url: string | null; role_id: string; role?: Role; + project_id: string | null; is_active: boolean; last_login: Date | null; created_at: Date; @@ -54,6 +56,7 @@ export interface JwtPayload { email: string; roleId: string; roleName: string; + projectId?: string | null; iat?: number; exp?: number; } diff --git a/water-api/src/validators/user.validator.ts b/water-api/src/validators/user.validator.ts index 8d059ae..2a22ac2 100644 --- a/water-api/src/validators/user.validator.ts +++ b/water-api/src/validators/user.validator.ts @@ -30,6 +30,11 @@ export const createUserSchema = z.object({ role_id: z .string({ required_error: 'Role ID is required' }) .uuid('Role ID must be a valid UUID'), + project_id: z + .string() + .uuid('Project ID must be a valid UUID') + .nullable() + .optional(), is_active: z.boolean().default(true), }); @@ -58,6 +63,11 @@ export const updateUserSchema = z.object({ .string() .uuid('Role ID must be a valid UUID') .optional(), + project_id: z + .string() + .uuid('Project ID must be a valid UUID') + .nullable() + .optional(), is_active: z.boolean().optional(), });