first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
# api-identity-domain
El usuario, tal como lo conoce TaskStream (sus datos vienen de Pireon).
- **Import**: `@task-stream/api/identity/domain`
- **Capa**: `domain` · **Tags**: `scope:api-identity`, `type:domain`
## Contenido
- `User` (agregado): nombre, email, activo, roles globales y roles en el cliente; `hasAppRole()`, `deactivate()`.
- `UserFactory`: construye usuarios a partir de Pireon o de la caché.
- Tipos del inicio de sesión: `TokenGrant`, `TokenSet`, `AuthorizationConfig`.
## Comandos
```sh
npx nx lint api-identity-domain
npx nx test api-identity-domain # Jest
```
Reglas de dependencia entre capas: [Arquitectura](../../../../docs/02-arquitectura.md#21-capas-hexagonales).

View File

@@ -0,0 +1,3 @@
import baseConfig from '../../../../eslint.config.mjs';
export default [...baseConfig];

View File

@@ -0,0 +1,11 @@
module.exports = {
displayName: 'api-identity-domain',
preset: '../../../../jest.preset.js',
testEnvironment: 'node',
passWithNoTests: true,
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../../coverage/libs/api/identity/domain',
};

View File

@@ -0,0 +1,12 @@
{
"name": "api-identity-domain",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/api/identity/domain/src",
"projectType": "library",
"tags": [
"scope:api-identity",
"type:domain"
],
"// targets": "to see all targets run: nx show project api-identity-domain --web",
"targets": {}
}

View File

@@ -0,0 +1,4 @@
export * from './lib/user';
export * from './lib/events/user-deactivated.event';
export * from './lib/factories/user.factory';
export * from './lib/token';

View File

@@ -0,0 +1,9 @@
import { AutowiredEvent, DomainEvent } from '@task-stream/api/shared/domain';
/** Pireon deactivated the user. */
@AutowiredEvent
export class UserDeactivatedEvent extends DomainEvent {
constructor(readonly userId: string) {
super('identity-server');
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { assertThat } from '@task-stream/api/shared/domain';
import { User, UserSnapshot } from '../user';
type UserProps = Omit<UserSnapshot, 'roles' | 'appRoles'> &
Partial<Pick<UserSnapshot, 'roles' | 'appRoles'>>;
/** Users are never created here (Pireon owns them): they are only (re)built. */
@Injectable()
export class UserFactory {
create(props: UserProps): User {
assertThat(props.id.length > 0, 'User id is required');
const user = new User(props.id);
user.name = props.name;
user.email = props.email;
user.isActive = props.isActive;
user.roles = [...(props.roles ?? [])];
user.appRoles = [...(props.appRoles ?? [])];
return user;
}
restore(snapshot: UserProps): User {
return this.create(snapshot);
}
}

View File

@@ -0,0 +1,27 @@
/** How a client proves who it is to get tokens from the identity server. */
export type TokenGrant =
| {
type: 'authorization_code';
code: string;
codeVerifier: string;
redirectUri: string;
}
| { type: 'password'; username: string; password: string }
| { type: 'refresh_token'; refreshToken: string };
export interface TokenSet {
accessToken: string;
refreshToken: string | null;
/** Seconds until the access token expires. */
expiresIn: number;
tokenType: string;
}
/** What a browser needs to start the Authorization Code + PKCE flow. */
export interface AuthorizationConfig {
authorizeUrl: string;
clientId: string;
scopes: string[];
/** Password grant can be disabled on the identity server. */
passwordGrant: boolean;
}

View File

@@ -0,0 +1,56 @@
import { VersionedAggregateRoot } from '@task-stream/api/shared/domain';
import { UserDeactivatedEvent } from './events/user-deactivated.event';
export interface UserSnapshot {
/** Pireon user id (string to stay provider-agnostic). */
id: string;
name: string;
email: string;
isActive: boolean;
/** Global roles in the identity server. */
roles: string[];
/** Roles granted inside this OAuth client (task-stream). */
appRoles: string[];
}
/**
* A person managed by the Pireon identity server. Not event-sourced here
* (Pireon owns users): UserFactory rebuilds it from Pireon or the local cache.
*/
export class User extends VersionedAggregateRoot implements UserSnapshot {
public name!: string;
public email!: string;
public isActive = true;
public roles: string[] = [];
public appRoles: string[] = [];
constructor(id: string) {
super();
this.id = id;
}
/** Global Pireon admins implicitly hold every app role. */
hasAppRole(role: string): boolean {
return this.appRoles.includes(role) || this.roles.includes('admin');
}
deactivate(): void {
if (!this.isActive) return;
this.apply(new UserDeactivatedEvent(this.id));
}
onUserDeactivatedEvent(): void {
this.isActive = false;
}
toSnapshot(): UserSnapshot {
return {
id: this.id,
name: this.name,
email: this.email,
isActive: this.isActive,
roles: [...this.roles],
appRoles: [...this.appRoles],
};
}
}

View File

@@ -0,0 +1,23 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": [
"jest.config.ts",
"jest.config.cts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "bundler",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"jest.config.cts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}