add user-project relation and role-based filtering

This commit is contained in:
2026-02-02 01:14:57 -06:00
parent 01aadcf2f3
commit 1d278936b1
5 changed files with 47 additions and 2 deletions

View File

@@ -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';

View File

@@ -41,6 +41,7 @@ export function authenticateToken(
email: (decoded as any).email, email: (decoded as any).email,
roleId: (decoded as any).roleId || (decoded as any).role, roleId: (decoded as any).roleId || (decoded as any).role,
roleName: (decoded as any).roleName || (decoded as any).role, roleName: (decoded as any).roleName || (decoded as any).role,
projectId: (decoded as any).projectId,
}; };
next(); next();

View File

@@ -55,9 +55,10 @@ export async function login(
password_hash: string; password_hash: string;
avatar_url: string | null; avatar_url: string | null;
role_name: string; role_name: string;
project_id: string | null;
created_at: Date; 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 FROM users u
JOIN roles r ON u.role_id = r.id JOIN roles r ON u.role_id = r.id
WHERE LOWER(u.email) = LOWER($1) AND u.is_active = true WHERE LOWER(u.email) = LOWER($1) AND u.is_active = true
@@ -83,6 +84,7 @@ export async function login(
email: user.email, email: user.email,
roleId: user.id, roleId: user.id,
roleName: user.role_name, roleName: user.role_name,
projectId: user.project_id,
}); });
const refreshToken = generateRefreshToken({ const refreshToken = generateRefreshToken({
@@ -170,8 +172,9 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
id: string; id: string;
email: string; email: string;
role_name: 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 FROM users u
JOIN roles r ON u.role_id = r.id JOIN roles r ON u.role_id = r.id
WHERE u.id = $1 AND u.is_active = true 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, email: user.email,
roleId: user.id, roleId: user.id,
roleName: user.role_name, roleName: user.role_name,
projectId: user.project_id,
}); });
return { accessToken }; return { accessToken };

View File

@@ -30,6 +30,7 @@ export interface User {
avatar_url: string | null; avatar_url: string | null;
role_id: string; role_id: string;
role?: Role; role?: Role;
project_id: string | null;
is_active: boolean; is_active: boolean;
last_login: Date | null; last_login: Date | null;
created_at: Date; created_at: Date;
@@ -43,6 +44,7 @@ export interface UserPublic {
avatar_url: string | null; avatar_url: string | null;
role_id: string; role_id: string;
role?: Role; role?: Role;
project_id: string | null;
is_active: boolean; is_active: boolean;
last_login: Date | null; last_login: Date | null;
created_at: Date; created_at: Date;
@@ -54,6 +56,7 @@ export interface JwtPayload {
email: string; email: string;
roleId: string; roleId: string;
roleName: string; roleName: string;
projectId?: string | null;
iat?: number; iat?: number;
exp?: number; exp?: number;
} }

View File

@@ -30,6 +30,11 @@ export const createUserSchema = z.object({
role_id: z role_id: z
.string({ required_error: 'Role ID is required' }) .string({ required_error: 'Role ID is required' })
.uuid('Role ID must be a valid UUID'), .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), is_active: z.boolean().default(true),
}); });
@@ -58,6 +63,11 @@ export const updateUserSchema = z.object({
.string() .string()
.uuid('Role ID must be a valid UUID') .uuid('Role ID must be a valid UUID')
.optional(), .optional(),
project_id: z
.string()
.uuid('Project ID must be a valid UUID')
.nullable()
.optional(),
is_active: z.boolean().optional(), is_active: z.boolean().optional(),
}); });