Merge pull request 'feat: Implement multi-tenancy support in middleware and security layers' (#15) from feature/security into development

Reviewed-on: ADUANASOFT/anexo76#15
This commit is contained in:
2026-01-20 18:54:10 +00:00
62 changed files with 6441 additions and 791 deletions

View File

@@ -1,26 +1,7 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface CustomsBroker {
type?: string | null;
broker_key: string;
name?: string | null;
address?: string | null;
postal_code?: string | null;
city?: string | null;
state?: string | null;
phone?: string | null;
fax?: string | null;
email?: string | null;
country?: string | null;
tax_id?: string | null;
personal_id?: string | null;
position?: string | null;
license: string;
company?: string | null;
contact?: string | null;
tenant_id: string;
company_id: string;
export interface CustomsBroker {
id: number;
type?: string | null;
broker_key: string;
@@ -117,13 +98,14 @@ export const customsBrokersApi = {
return api.post<CustomsBroker>(`/v1/a76/customs-brokers/?company_id=${companyId}`, data);
},
update: (brokerKey: string, data: CreateCustomsBrokerData, companyId: string) => {
return api.patch<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`, data);
},
/**
* Actualiza la información de un agente aduanal
*/
update: (brokerKey: string, data: CreateCustomsBrokerData) => {
const companyId = data.company_id;
return api.put<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data);
},
delete: (brokerKey: string, companyId: string) => {
return api.delete<void>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
},
/**
* Elimina un agente aduanal
*/
@@ -131,13 +113,7 @@ export const customsBrokersApi = {
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`);
},
/**
* Actualiza la información de un agente aduanal
*/
update: (brokerKey: string, data: CreateCustomsBrokerData) => {
const companyId = data.company_id;
return api.put<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data);
},
updateVU: (brokerKey: string, data: CustomsBrokerVU, companyId: string) => {
return api.put<CustomsBrokerVU>(`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${companyId}`, data);

View File

@@ -0,0 +1,8 @@
/**
* Exportaciones centralizadas de APIs de administración
*/
export * from './permissions';
export * from './roles';
export * from './role-permissions';
export * from './user-roles';

View File

@@ -0,0 +1,110 @@
/**
* API para gestión de permisos del sistema
*/
import { api } from '$lib/api';
export interface Permission {
id: number;
code: string;
description?: string;
module: string;
action: string;
is_active: boolean;
created_at?: string;
updated_at?: string;
}
export interface CreatePermissionData {
code?: string;
description?: string;
module: string;
action: string;
is_active?: boolean;
}
export interface UpdatePermissionData {
code?: string;
description?: string;
module?: string;
action?: string;
is_active?: boolean;
}
export interface PermissionListResponse {
items: Permission[];
total: number;
page: number;
page_size: number;
}
export const permissionsAPI = {
/**
* Listar permisos con filtros
*/
async list(params?: {
page?: number;
page_size?: number;
module?: string;
action?: string;
is_active?: boolean;
search?: string;
}): Promise<PermissionListResponse> {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.page_size) queryParams.set('page_size', params.page_size.toString());
if (params?.module) queryParams.set('module', params.module);
if (params?.action) queryParams.set('action', params.action);
if (params?.search) queryParams.set('search', params.search);
const query = queryParams.toString();
const response = await api.get(`/v1/core/permissions${query ? '?' + query : ''}`);
return response.data;
},
/**
* Obtener un permiso por ID
*/
async getById(id: number): Promise<Permission> {
const response = await api.get(`/v1/core/permissions/${id}`);
return response.data;
},
/**
* Crear un nuevo permiso
*/
async create(data: CreatePermissionData): Promise<Permission> {
const response = await api.post('/v1/core/permissions', data);
return response.data;
},
/**
* Actualizar un permiso
*/
async update(id: number, data: UpdatePermissionData): Promise<Permission> {
const response = await api.put(`/v1/core/permissions/${id}`, data);
return response.data;
},
/**
* Eliminar un permiso
*/
async delete(id: number): Promise<void> {
await api.delete(`/v1/core/permissions/${id}`);
},
/**
* Obtener módulos únicos
*/
async getModules(): Promise<string[]> {
const response = await api.get('/v1/core/permissions/modules');
return response.data;
},
/**
* Obtener acciones únicas
*/
async getActions(): Promise<string[]> {
const response = await api.get('/v1/core/permissions/actions');
return response.data;
}
};

View File

@@ -0,0 +1,76 @@
/**
* API para gestión de permisos de roles
*/
import { api } from '$lib/api';
export interface RolePermission {
id: number;
company_role_id: number;
permission_id: number;
granted_at?: string;
granted_by?: number;
tenant_id: number;
permission?: {
id: number;
code: string;
module: string;
action: string;
description?: string;
is_active: boolean;
};
}
export interface AssignPermissionData {
permission_id: number;
}
export interface RolePermissionsResponse {
role_id: number;
permissions: RolePermission[];
total: number;
}
export const rolePermissionsAPI = {
/**
* Listar todos los permisos asignados a un rol
*/
async listByRole(roleId: number, companyId: number): Promise<RolePermissionsResponse> {
const response = await api.get(`/v1/core/permissions/roles/${roleId}/permissions?company_id=${companyId}`);
return response.data;
},
/**
* Asignar un permiso a un rol
*/
async assign(
roleId: number,
companyId: number,
data: AssignPermissionData
): Promise<RolePermission> {
const response = await api.post(`/v1/core/permissions/roles/${roleId}/permissions?company_id=${companyId}`, data);
return response.data;
},
/**
* Remover un permiso de un rol
*/
async remove(roleId: number, permissionId: number, companyId: number): Promise<void> {
await api.delete(`/v1/core/permissions/roles/${roleId}/permissions/${permissionId}?company_id=${companyId}`);
},
/**
* Asignar múltiples permisos a un rol
*/
async assignMultiple(
roleId: number,
companyId: number,
permissionIds: number[]
): Promise<RolePermission[]> {
const response = await api.post(
`/v1/core/permissions/roles/${roleId}/permissions/batch?company_id=${companyId}`,
{ permission_ids: permissionIds }
);
return response.data;
}
};

View File

@@ -0,0 +1,93 @@
/**
* API para gestión de roles por compañía
*/
import { api } from '$lib/api';
export interface CompanyRole {
id: number;
name: string;
code: string;
description?: string;
is_active: boolean;
company_id: number;
tenant_id: number;
created_at?: string;
updated_at?: string;
}
export interface CreateRoleData {
name: string;
code: string;
description?: string;
is_active?: boolean;
}
export interface UpdateRoleData {
name?: string;
code?: string;
description?: string;
is_active?: boolean;
}
export interface RoleListResponse {
items: CompanyRole[];
total: number;
page: number;
page_size: number;
}
export const rolesAPI = {
/**
* Listar roles con filtros
*/
async list(
companyId: number,
params?: {
page?: number;
page_size?: number;
is_active?: boolean;
search?: string;
}
): Promise<RoleListResponse> {
const queryParams = new URLSearchParams();
queryParams.set('company_id', companyId.toString());
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.page_size) queryParams.set('page_size', params.page_size.toString());
if (params?.is_active !== undefined) queryParams.set('is_active', params.is_active.toString());
if (params?.search) queryParams.set('search', params.search);
const response = await api.get(`/v1/core/permissions/roles?${queryParams.toString()}`);
return response.data;
},
/**
* Obtener un rol por ID
*/
async getById(id: number, companyId: number): Promise<CompanyRole> {
const response = await api.get(`/v1/core/permissions/roles/${id}?company_id=${companyId}`);
return response.data;
},
/**
* Crear un nuevo rol
*/
async create(companyId: number, data: CreateRoleData): Promise<CompanyRole> {
const response = await api.post(`/v1/core/permissions/roles?company_id=${companyId}`, data);
return response.data;
},
/**
* Actualizar un rol
*/
async update(id: number, companyId: number, data: UpdateRoleData): Promise<CompanyRole> {
const response = await api.patch(`/v1/core/permissions/roles/${id}?company_id=${companyId}`, data);
return response.data;
},
/**
* Eliminar un rol
*/
async delete(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/core/permissions/roles/${id}?company_id=${companyId}`);
}
};

View File

@@ -0,0 +1,108 @@
/**
* API para gestión de permisos individuales de usuario
*/
import { api } from '$lib/api';
import type { Permission } from './permissions';
export interface UserPermission {
id: number;
user_id: string;
permission_id: number;
company_id: number;
tenant_id: number;
is_granted: boolean;
is_active: boolean;
assigned_by?: string;
expires_at?: string;
created_at?: string;
updated_at?: string;
permission?: Permission;
}
export interface AssignUserPermissionData {
user_id: string;
permission_id: number;
is_granted?: boolean;
expires_at?: string;
}
export interface UserPermissionsListResponse {
items: UserPermission[];
total: number;
}
export interface EffectiveUserPermissions {
user_id: string;
company_id: number;
role_permissions: Permission[];
granted_permissions: Permission[];
revoked_permissions: Permission[];
effective_permissions: Permission[];
}
export const userPermissionsAPI = {
/**
* Obtener permisos individuales de un usuario
*/
async getIndividual(userId: string, companyId: number): Promise<UserPermissionsListResponse> {
const response = await api.get(
`/v1/core/permissions/users/${userId}/permissions?company_id=${companyId}`
);
return response.data;
},
/**
* Obtener permisos efectivos de un usuario (roles + individuales - revocados)
*/
async getEffective(userId: string, companyId: number): Promise<EffectiveUserPermissions> {
const response = await api.get(
`/v1/core/permissions/users/${userId}/permissions/effective?company_id=${companyId}`
);
return response.data;
},
/**
* Asignar un permiso individual a un usuario
*/
async assign(
userId: string,
companyId: number,
data: Omit<AssignUserPermissionData, 'user_id'>
): Promise<UserPermission> {
const response = await api.post(
`/v1/core/permissions/users/${userId}/permissions?company_id=${companyId}`,
data
);
return response.data;
},
/**
* Conceder un permiso extra a un usuario
*/
async grant(userId: string, companyId: number, permissionId: number): Promise<UserPermission> {
return this.assign(userId, companyId, {
permission_id: permissionId,
is_granted: true
});
},
/**
* Revocar un permiso específico (aunque venga del rol)
*/
async revoke(userId: string, companyId: number, permissionId: number): Promise<UserPermission> {
return this.assign(userId, companyId, {
permission_id: permissionId,
is_granted: false
});
},
/**
* Eliminar un permiso individual
*/
async remove(userId: string, companyId: number, permissionId: number): Promise<void> {
await api.delete(
`/v1/core/permissions/users/${userId}/permissions/${permissionId}?company_id=${companyId}`
);
}
};

View File

@@ -0,0 +1,94 @@
/**
* API para gestión de roles de usuarios
*/
import { api } from '$lib/api';
export interface UserRole {
id: number;
user_id: string;
company_id: number;
company_role_id: number;
is_active: boolean;
created_at: string;
assigned_by?: string;
company_role?: {
id: number;
name: string;
code: string;
description?: string;
};
user?: {
id: number;
username: string;
email?: string;
full_name?: string;
};
}
export interface AssignUserRoleData {
user_id: string;
company_role_id: number;
}
export interface UserRolesResponse {
items: UserRole[];
total: number;
page: number;
page_size: number;
}
export const userRolesAPI = {
/**
* Listar todos los roles asignados a usuarios
*/
async list(
companyId: number,
params?: {
user_id?: string;
company_role_id?: number;
page?: number;
page_size?: number;
}
): Promise<UserRolesResponse> {
const queryParams = new URLSearchParams();
queryParams.set('company_id', companyId.toString());
if (params?.user_id) queryParams.set('user_id', params.user_id);
if (params?.company_role_id) queryParams.set('company_role_id', params.company_role_id.toString());
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.page_size) queryParams.set('page_size', params.page_size.toString());
const response = await api.get(`/v1/core/permissions/user-roles?${queryParams.toString()}`);
return response.data;
},
/**
* Listar roles de un usuario específico
*/
async listByUser(userId: number, companyId: number): Promise<UserRolesResponse> {
const response = await api.get(`/v1/core/permissions/users/${userId}/roles?company_id=${companyId}`);
return response.data;
},
/**
* Listar usuarios con un rol específico
*/
async listByRole(roleId: number, companyId: number): Promise<UserRolesResponse> {
const response = await api.get(`/v1/core/permissions/roles/${roleId}/users?company_id=${companyId}`);
return response.data;
},
/**
* Asignar un rol a un usuario
*/
async assign(companyId: number, data: AssignUserRoleData): Promise<UserRole> {
const response = await api.post(`/v1/core/permissions/user-roles?company_id=${companyId}`, data);
return response.data;
},
/**
* Remover un rol de un usuario
*/
async remove(userRoleId: number, companyId: number): Promise<void> {
await api.delete(`/v1/core/permissions/user-roles/${userRoleId}?company_id=${companyId}`);
}
};

View File

@@ -62,8 +62,8 @@ export const usersAPI = {
/**
* Obtiene estadísticas de usuarios del tenant
*/
async getStats(): Promise<UserStats> {
const response = await api.get<UserStats>('/v1/core/users/stats');
async getStats(companyId: number): Promise<UserStats> {
const response = await api.get<UserStats>(`/v1/core/users/stats?company_id=${companyId}`);
if (response.error) {
throw new Error(response.error);
}
@@ -73,17 +73,18 @@ export const usersAPI = {
/**
* Lista usuarios del tenant con paginación
*/
async list(params?: {
async list(companyId: number, params?: {
page?: number;
page_size?: number;
search?: string;
}): Promise<UserListResponse> {
const queryParams = new URLSearchParams();
queryParams.set('company_id', companyId.toString());
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.page_size) queryParams.set('page_size', params.page_size.toString());
if (params?.search) queryParams.set('search', params.search);
const endpoint = `/v1/core/users/${queryParams.toString() ? `?${queryParams}` : ''}`;
const endpoint = `/v1/core/users/?${queryParams}`;
const response = await api.get<UserListResponse>(endpoint);
if (response.error) {
throw new Error(response.error);
@@ -94,8 +95,8 @@ export const usersAPI = {
/**
* Obtiene un usuario específico
*/
async get(userId: string): Promise<User> {
const response = await api.get<User>(`/v1/core/users/${userId}`);
async get(userId: string, companyId: number): Promise<User> {
const response = await api.get<User>(`/v1/core/users/${userId}?company_id=${companyId}`);
if (response.error) {
throw new Error(response.error);
}
@@ -105,8 +106,8 @@ export const usersAPI = {
/**
* Crea un nuevo usuario
*/
async create(data: CreateUserRequest): Promise<User> {
const response = await api.post<User>('/v1/core/users/', data);
async create(data: CreateUserRequest, companyId: number): Promise<User> {
const response = await api.post<User>(`/v1/core/users/?company_id=${companyId}`, data);
if (response.error) {
throw new Error(response.error);
}
@@ -116,8 +117,8 @@ export const usersAPI = {
/**
* Actualiza un usuario existente
*/
async update(userId: string, data: UpdateUserRequest): Promise<User> {
const response = await api.put<User>(`/v1/core/users/${userId}`, data);
async update(userId: string, data: UpdateUserRequest, companyId: number): Promise<User> {
const response = await api.put<User>(`/v1/core/users/${userId}?company_id=${companyId}`, data);
if (response.error) {
throw new Error(response.error);
}
@@ -127,8 +128,9 @@ export const usersAPI = {
/**
* Elimina un usuario
*/
async delete(userId: string, softDelete: boolean = true): Promise<void> {
async delete(userId: string, companyId: number, softDelete: boolean = true): Promise<void> {
const queryParams = new URLSearchParams();
queryParams.set('company_id', companyId.toString());
queryParams.set('soft_delete', softDelete.toString());
const response = await api.delete(`/v1/core/users/${userId}?${queryParams}`);
@@ -140,8 +142,8 @@ export const usersAPI = {
/**
* Cambia la contraseña de un usuario
*/
async changePassword(userId: string, data: ChangePasswordRequest): Promise<void> {
const response = await api.post(`/v1/core/users/${userId}/change-password`, data);
async changePassword(userId: string, data: ChangePasswordRequest, companyId: number): Promise<void> {
const response = await api.post(`/v1/core/users/${userId}/change-password?company_id=${companyId}`, data);
if (response.error) {
throw new Error(response.error);
}