feat: plantilla base workspace SaaS
Some checks failed
Build Producción & Push a Harbor / test (push) Failing after 3s
Build Producción & Push a Harbor / build (push) Has been skipped
Aduanasoft/plantillas-proyectos/pipeline/head There was a failure building this commit

This commit is contained in:
2026-07-21 13:59:00 -05:00
commit bdd089954b
470 changed files with 70022 additions and 0 deletions

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,89 @@
/**
* API para gestión de roles por compañía
*/
import { api, type ApiResponse } 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<ApiResponse<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);
return api.get(`/v1/core/permissions/roles?${queryParams.toString()}`);
},
/**
* Obtener un rol por ID
*/
async getById(id: number, companyId: number): Promise<ApiResponse<CompanyRole>> {
return api.get(`/v1/core/permissions/roles/${id}?company_id=${companyId}`);
},
/**
* Crear un nuevo rol
*/
async create(companyId: number, data: CreateRoleData): Promise<ApiResponse<CompanyRole>> {
return api.post(`/v1/core/permissions/roles?company_id=${companyId}`, data);
},
/**
* Actualizar un rol
*/
async update(id: number, companyId: number, data: UpdateRoleData): Promise<ApiResponse<CompanyRole>> {
return api.patch(`/v1/core/permissions/roles/${id}?company_id=${companyId}`, data);
},
/**
* Eliminar un rol
*/
async delete(id: number, companyId: number): Promise<ApiResponse<void>> {
return 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

@@ -0,0 +1 @@
export type { KPIMetric, ActivityItem, ChartDataPoint } from './types';

View File

@@ -0,0 +1,60 @@
import { api } from '$lib/api';
export interface InviteCode {
id: number;
code: string;
tenant_slug: string;
company_id: number | null;
role: string;
max_uses: number | null;
uses_count: number;
expires_at: string | null;
is_active: boolean;
created_by: string;
created_at: string;
}
export interface CreateInviteCodeRequest {
company_id?: number | null;
role: string;
max_uses?: number | null;
expires_at?: string | null;
}
export interface ValidateInviteCodeResponse {
code: string;
tenant_slug: string;
company_id: number | null;
role: string;
remaining_uses: number | null;
expires_at: string | null;
}
export const inviteCodesAPI = {
async list(companyId: number, includeInactive = false): Promise<InviteCode[]> {
const params = new URLSearchParams({ company_id: companyId.toString() });
if (includeInactive) params.set('include_inactive', 'true');
const response = await api.get<InviteCode[]>(`/v1/core/invite-codes?${params}`);
if (response.error) throw new Error(response.error);
return response.data!;
},
async create(data: CreateInviteCodeRequest): Promise<InviteCode> {
const response = await api.post<InviteCode>('/v1/core/invite-codes', data);
if (response.error) throw new Error(response.error);
return response.data!;
},
async revoke(code: string): Promise<void> {
const response = await api.delete(`/v1/core/invite-codes/${code}`);
if (response.error) throw new Error(response.error);
},
async validate(code: string): Promise<ValidateInviteCodeResponse> {
const response = await api.get<ValidateInviteCodeResponse>(
`/v1/core/invite-codes/validate/${code}`
);
if (response.error) throw new Error(response.error);
return response.data!;
}
};

View File

@@ -0,0 +1,24 @@
export interface KPIMetric {
label: string;
value: number;
previous_value?: number;
percentage_change?: number;
trend?: 'up' | 'down' | 'stable';
unit?: string;
}
export interface ActivityItem {
id: number;
type: string;
title: string;
description?: string;
timestamp: string;
status?: string;
icon?: string;
}
export interface ChartDataPoint {
label: string;
value: number;
category?: string;
}

View File

@@ -0,0 +1,196 @@
/**
* Cliente API para gestión de usuarios
*/
import { api } from '$lib/api';
export interface User {
id: string;
username: string;
email: string;
first_name: string;
last_name: string;
enabled: boolean;
email_verified: boolean;
created_timestamp?: number;
role?: string;
}
export interface UserStats {
total_users: number;
active_users: number;
inactive_users: number;
max_users_allowed: number;
users_available: number;
usage_percentage: number;
}
export interface UserListResponse {
users: User[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
export interface CreateUserRequest {
email: string;
username: string;
first_name: string;
last_name: string;
password: string;
role?: string;
enabled?: boolean;
email_verified?: boolean;
}
export interface UpdateUserRequest {
first_name?: string;
last_name?: string;
email?: string;
enabled?: boolean;
email_verified?: boolean;
role?: string;
}
export interface ChangePasswordRequest {
password: string;
temporary?: boolean;
}
export interface InviteUserRequest {
email: string;
company_id: number;
role_id: number;
}
export interface InviteUserResponse {
id: number;
email: string;
role: string;
expires_at: string;
invite_url: string;
created_at: string;
}
export const usersAPI = {
/**
* Obtiene estadísticas de usuarios del tenant
*/
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);
}
return response.data!;
},
/**
* Lista usuarios del tenant con paginación
*/
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}`;
const response = await api.get<UserListResponse>(endpoint);
if (response.error) {
throw new Error(response.error);
}
return response.data!;
},
/**
* Obtiene un usuario específico
*/
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);
}
return response.data!;
},
/**
* Crea un nuevo usuario
*/
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);
}
return response.data!;
},
/**
* Actualiza un usuario existente
*/
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);
}
return response.data!;
},
/**
* Retorna en cuántos tenants está registrado el usuario.
*/
async getTenantCount(userId: string, companyId: number): Promise<number> {
const response = await api.get<{ tenant_count: number }>(
`/v1/core/users/${userId}/tenant-count?company_id=${companyId}`
);
if (response.error) {
throw new Error(response.error);
}
return response.data!.tenant_count;
},
/**
* Elimina un usuario
*/
async delete(
userId: string,
companyId: number,
softDelete: boolean = true,
scope: 'current' | 'all' = 'current'
): Promise<void> {
const queryParams = new URLSearchParams();
queryParams.set('company_id', companyId.toString());
queryParams.set('soft_delete', softDelete.toString());
queryParams.set('scope', scope);
const response = await api.delete(`/v1/core/users/${userId}?${queryParams}`);
if (response.error) {
throw new Error(response.error);
}
},
/**
* Cambia la contraseña de un usuario
*/
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);
}
},
/**
* Genera un token de invitación y envía email al usuario
*/
async invite(data: InviteUserRequest): Promise<InviteUserResponse> {
const response = await api.post<InviteUserResponse>('/v1/core/invites', data);
if (response.error) {
throw new Error(response.error);
}
return response.data!;
}
};

View File

@@ -0,0 +1,128 @@
import { getToken, authStore } from '$lib/auth';
import { get } from 'svelte/store';
const api_url = import.meta.env.VITE_API_URL ?? '';
const normalizedApiUrl = api_url ? (api_url.endsWith('/') ? api_url : `${api_url}/`) : '/';
const BASE_URL = `${normalizedApiUrl}v1/core/help-center`;
function getAuthToken(): string | null {
// 1. First try getToken() which checks Keycloak and localStorage
let token = getToken();
// 2. If somehow empty, explicitly check authStore value
if (!token) {
const auth = get(authStore);
token = auth.token;
}
return token;
}
function getHeaders() {
const token = getAuthToken();
return {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
};
}
export interface HelpArticle {
uuid: string;
slug: string;
title: string;
content: string;
updated_at: string;
last_editor: string;
category?: string;
order?: number;
content_type: string;
file_url?: string;
file_size?: number;
mime_type?: string;
context_path?: string;
tags?: string;
}
export const helpApi = {
async listArticles(): Promise<HelpArticle[]> {
const response = await fetch(`${BASE_URL}/articles/`, { headers: getHeaders() });
if (!response.ok) throw new Error('Failed to fetch articles');
return response.json();
},
async getArticle(uuid: string): Promise<HelpArticle> {
const response = await fetch(`${BASE_URL}/articles/${uuid}/`, { headers: getHeaders() });
if (!response.ok) throw new Error('Failed to fetch article');
return response.json();
},
async updateArticle(uuid: string, data: Partial<HelpArticle>): Promise<HelpArticle> {
const response = await fetch(`${BASE_URL}/articles/${uuid}/`, {
method: 'PATCH',
headers: getHeaders(),
body: JSON.stringify(data)
});
if (response.status === 403) throw new Error('No tienes permisos para editar artículos (Requiere rol Admin)');
if (!response.ok) throw new Error('Error al guardar cambios');
return response.json();
},
async createArticle(data: Partial<HelpArticle>): Promise<HelpArticle> {
const response = await fetch(`${BASE_URL}/articles/`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(data)
});
if (response.status === 403) throw new Error('No tienes permisos para crear artículos (Requiere rol Admin)');
if (!response.ok) throw new Error('Error al crear el artículo');
return response.json();
},
async deleteArticle(uuid: string): Promise<void> {
const response = await fetch(`${BASE_URL}/articles/${uuid}/`, {
method: 'DELETE',
headers: getHeaders()
});
if (response.status === 403) throw new Error('No tienes permisos para eliminar (Requiere rol Admin)');
if (!response.ok) throw new Error('Error al eliminar');
},
async triggerSync(): Promise<void> {
// Opcional: endpoint para forzar sync desde UI si es necesario
},
async uploadImage(file: File): Promise<{ url: string }> {
const formData = new FormData();
formData.append('file', file);
const token = getAuthToken();
const response = await fetch(`${BASE_URL}/upload-image/`, {
method: 'POST',
// No Content-Type header for FormData, browser sets it with boundary
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: formData
});
if (response.status === 403) throw new Error('No tienes permisos para subir imágenes (Requiere rol Admin)');
if (!response.ok) throw new Error('Error al subir imagen');
return response.json();
},
async uploadAsset(file: File): Promise<{ url: string, filename: string, size: number, mime_type: string }> {
const formData = new FormData();
formData.append('file', file);
const token = getAuthToken();
const response = await fetch(`${BASE_URL}/upload-asset/`, {
method: 'POST',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: formData
});
if (response.status === 403) throw new Error('No tienes permisos para subir archivos (Requiere rol Admin)');
if (!response.ok) throw new Error('Error al subir archivo');
return response.json();
}
};