Estructura completa de la librería compartida que contiene data-access (autenticación, HTTP, interceptores), master (StateService), ui (componentes reutilizables), utils (constantes, helpers, pipes, validators) y models (tipos comunes).
🔐 AuthService + Guards
🌐 HttpService + Interceptors
💾 StateService (persistencia)
🎨 UI Components
🛠️ Utils + Pipes + Validators
auth/auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
handleSuccessfulAuth(content: AuthContent): void {
localStorage.setItem('access\_token', content.accessToken);
localStorage.setItem('refresh\_token', content.refreshToken);
this.isAuthenticatedSubject.next(true);
this.scheduleTokenRefresh();
}
refreshToken(): Observable<AuthContent> {
return this.http.post(\`${this.AUTH\_ENDPOINT}/refresh\`, { refreshToken });
}
handle401Error<T>(req: HttpRequest<T>, next: HttpHandlerFn) {
// Cola de peticiones durante refresh
}
validateTokenAndGetSession(): Observable<UserData> { }
updateTokensAfterCompanySelection(accessToken, refreshToken, expiresIn): void { }
}
🔐 Servicio central de autenticación
auth/auth.guard.ts + login.guard.ts
// authGuard - Protege rutas autenticadas
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isSessionActive()) return true;
authService.clearAllData();
router.navigate(\['/login'\]);
return false;
};
// loginGuard - Previene acceso a login si ya hay sesión
export const loginGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isSessionActive()) {
router.navigate(\['/layout'\]);
return false;
}
return true;
};
🛡️ Protección de rutas y redirección inteligente
✔️ authGuard: verifica sesión activa, redirige a login si expiró.
✔️ loginGuard: si ya hay sesión, redirige a layout (evita doble login).
✔️ isSessionActive(): valida token + expiry.
auth/token.interceptor.ts + http/error.interceptor.ts
// tokenInterceptor - Añade Authorization header
export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
const publicUrls = \['/login', '/refresh', '/jwt', '/jwt-get'\];
if (publicUrls.some(url => req.url.includes(url))) return next(req);
const token = inject(AuthService).getToken();
if (token) {
const authReq = req.clone({
setHeaders: { Authorization: \`Bearer ${token}\`, Accept: 'application/json' }
});
return next(authReq);
}
return next(req);
};
// errorInterceptor - Manejo global de errores 401
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error) => {
if (error.status === 401 && !publicUrls.some(u => req.url.includes(u))) {
return inject(AuthService).handle401Error(req, next);
}
return throwError(() => error);
})
);
};
Interceptores HTTP funcionales (Angular 17+)
✔️ tokenInterceptor: añade Bearer token a todas las peticiones (excepto endpoints públicos).
✔️ errorInterceptor: captura 401 y delega en AuthService.handle401Error().
✔️ Public URLs: login, refresh, jwt, jwt-get no requieren token.
http/http.service.ts
@Injectable({ providedIn: 'root' })
export class HttpService {
private readonly http = inject(HttpClient);
get<T>(endpoint: string, params?: Record<string, unknown>): Observable<ApiResponse<T>>
post<T>(endpoint: string, body: unknown, params?: Record<string, unknown>): Observable<ApiResponse<T>>
getContent<T>(endpoint: string, params?: Record<string, string>): Observable<T>
}
** Wrapper tipado de HttpClient**
✔️ get/post/put/patch/delete: métodos con tipado genérico.
✔️ getContent: shortcut que extrae response.response.content automáticamente.
✔️ Manejo de parámetros: convierte objetos a HttpParams.
✔️ Integración con ApiResponse: estructura estandarizada de respuestas del backend.
http/api-response.model.ts + auth/auth.models.ts
// Estructura estandarizada de respuestas
export interface ApiResponse<T> {
response: {
length: number;
metadata: ApiResponseMetadata;
content: T;
};
}
export interface AuthContent {
accessToken: string;
refreshToken: string;
expiresIn: number;
sessionId: string;
}
export interface UserData {
id: number;
username: string;
email: string;
roles: Array<{ id: number; name: string; authority: string }>;
customerId: number;
companyId: number;
}
📦 Tipado completo para comunicación con backend
✔️ ApiResponse: envoltura estándar con metadata (timestamp, apiVersion, processingTime).
✔️ AuthContent: respuesta de login/refresh.
✔️ UserData: información del usuario desde JWT.
✔️ Inyección de tokens: API_BASE_URL_TOKEN y AUTH_CONFIG_TOKEN para configuración multi-entorno.
master/state.service.ts
@Injectable({ providedIn: 'root' })
export class StateService {
saveState<T>(tableId: string, state: Partial<T>): void {
const key = \`state\_${tableId}\`;
const dataToSave = { ...state, \_timestamp: Date.now() };
localStorage.setItem(key, JSON.stringify(dataToSave));
}
getState(tableId: string) {
const saved = localStorage.getItem(\`state\_${tableId}\`);
if (!saved) return null;
const parsed = JSON.parse(saved);
// Expira después de 24 horas
if (parsed.\_timestamp && Date.now() - parsed.\_timestamp > 86400000) {
this.clearState(tableId);
return null;
}
delete parsed.\_timestamp;
return parsed;
}
clearState(tableId: string): void;
clearAllStates(): void;
}
** Persistencia de estado de UI (tablas, búsquedas)**
✔️ Uso principal: guardar estado de tablas (búsqueda, ordenamiento, paginación).
✔️ Expiración automática: 24 horas para evitar datos obsoletos.
✔️ Identificador único: cada tabla/componente tiene su propio tableId.
✔️ Métodos: saveState, getState, clearState, clearAllStates.
✔️ Integración: usado en ProductsComponent, CompanyComponent y futuras tablas.
ui/ (nebula-ui-kit wrappers)
// Estructura típica de componentes UI
// (Nota: la mayoría de componentes vienen de nebula-ui-kit,
// esta librería puede extender o personalizarlos)
// Ejemplo de componente wrapper personalizado:
// src/lib/ui/loading-spinner/loading-spinner.component.ts
@Component({
selector: 'lib-loading-spinner',
template: \`<div class="spinner"></div>\`,
standalone: true
})
export class LoadingSpinnerComponent {}
🎨 Componentes visuales reutilizables
✔️ Base: nebula-ui-kit provee componentes como Button, Input, Table, Tabs, GridCard, Alert.
✔️ Extensiones: esta librería puede contener wrappers personalizados o componentes específicos del dominio.
✔️ Standalone: todos los componentes son standalone para mejor tree-shaking.
✔️ Ejemplos potenciales: EmptyState, PageHeader, ConfirmDialog, LoadingOverlay.
utils/constants/
// constants/app.constants.ts
export const APP\_CONFIG = {
VERSION: '1.0.0',
DEFAULT\_LANGUAGE: 'es',
TOKEN\_EXPIRY\_BUFFER\_MS: 30000, // 30 segundos
};
// constants/regex.constants.ts
export const REGEX\_PATTERNS = {
EMAIL: /^\[a-zA-Z0-9.\_%+-\]+@\[a-zA-Z0-9.-\]+\\.\[a-zA-Z\]{2,}$/,
NIT: /^\\d{9,10}$/,
PHONE: /^\[0-9+\\-\\s()\]{7,15}$/,
};
// constants/routes.constants.ts
export const PUBLIC\_ROUTES = \['/login', '/register', '/forgot-password'\];
** Constantes centralizadas**
✔️ App config: versión, idioma por defecto, buffers de tiempo.
✔️ Regex patterns: validaciones de email, NIT, teléfono, etc.
✔️ Routes constants: rutas públicas, protegidas, módulos.
✔️ Environment flags: dev, prod, test.
utils/helpers/
// helpers/date.helper.ts
export class DateHelper {
static formatDate(date: Date | string, format = 'dd/MM/yyyy'): string { }
static getAge(birthDate: Date): number { }
}
// helpers/string.helper.ts
export class StringHelper {
static capitalize(str: string): string { }
static slugify(str: string): string { }
static truncate(str: string, length: number): string { }
}
// helpers/storage.helper.ts
export class StorageHelper {
static setItem(key: string, value: any): void { }
static getItem(key: string): T | null { }
static removeItem(key: string): void { }
}
** Funciones auxiliares reutilizables**
✔️ DateHelper: formateo de fechas, cálculo de edad, diferencias.
✔️ StringHelper: capitalizar, slugify, truncar, limpiar.
✔️ StorageHelper: wrapper tipado para localStorage/sessionStorage.
✔️ ObjectHelper: deep clone, merge, isEmpty.
utils/pipes/
// pipes/currency-format.pipe.ts
@Pipe({ name: 'currencyFormat', standalone: true })
export class CurrencyFormatPipe implements PipeTransform {
transform(value: number, currency = 'COP'): string {
return new Intl.NumberFormat('es-CO', { style: 'currency', currency }).format(value);
}
}
// pipes/date-format.pipe.ts
@Pipe({ name: 'dateFormat', standalone: true })
export class DateFormatPipe implements PipeTransform {
transform(value: Date | string, format = 'dd/MM/yyyy'): string { }
}
// pipes/truncate.pipe.ts
@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50, suffix = '...'): string { }
}
Pipes de transformación en templates
✔️ CurrencyFormatPipe: formatea moneda (COP, USD).
✔️ DateFormatPipe: formatea fechas en templates.
✔️ TruncatePipe: acorta textos largos.
✔️ SafeHtmlPipe: sanitiza HTML dinámico.
✔️ Todos standalone: listos para usar en cualquier componente.
utils/validators/
// validators/custom.validators.ts
export class CustomValidators {
static nit(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const nitRegex = /^\\d{9,10}$/;
return nitRegex.test(value) ? null : { invalidNit: true };
}
static matchPasswords(passwordKey: string, confirmKey: string) {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get(passwordKey)?.value;
const confirm = group.get(confirmKey)?.value;
return password === confirm ? null : { passwordMismatch: true };
};
}
static phone(control: AbstractControl): ValidationErrors | null {
const phoneRegex = /^\[0-9+\\-\\s()\]{7,15}$/;
return phoneRegex.test(control.value) ? null : { invalidPhone: true };
}
}
Validadores reactivos reutilizables
✔️ nit: valida formato de NIT colombiano.
✔️ matchPasswords: verifica que contraseñas coincidan (para formularios de registro/cambio).
✔️ phone: valida números de teléfono.
✔️ Fácil integración: Validators.compose([Validators.required, CustomValidators.nit]).
models/
// models/user.model.ts
export interface User {
id: string;
username: string;
email: string;
fullName: string;
roles: UserRole\[\];
avatar?: string;
}
// models/common.model.ts
export interface PaginatedResult<T> {
items: T\[\];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface ApiError {
statusCode: number;
message: string;
error: string;
timestamp: string;
}
export type SortDirection = 'asc' | 'desc';
export interface Sort { field: string; direction: SortDirection; }
** Tipos compartidos entre módulos**
✔️ User: modelo base de usuario (usado en múltiples módulos).
✔️ PaginatedResult: envoltura para respuestas paginadas.
✔️ ApiError: estructura estandarizada de errores.
✔️ Sort, Filter, QueryParams: tipos para búsquedas y ordenamiento.
✔️ Reutilización: evita duplicación de interfaces entre módulos.
🔐 data-access/
AuthService, HttpService, interceptores, guards, modelos de API
💾 master/
StateService (persistencia de UI en localStorage)
🎨 ui/
Componentes reutilizables (wrappers, loaders, modales)
🛠️ utils/
Constantes, helpers, pipes, validators personalizados
📦 models/
Tipos e interfaces compartidas entre módulos
Dependencias y uso: Esta librería es consumida por todos los módulos de negocio (accounting, treasury, budget, products, company, login) y por la aplicación shell. Provee la capa de comunicación HTTP, autenticación, persistencia de estado y utilidades comunes.
nx test shared → pruebas unitarias para servicios, pipes y validators.
@nebula/shared · Librería core que unifica autenticación, HTTP, interceptores, persistencia de estado, componentes UI, utilidades y modelos para todo el ecosistema Nebula ERP.