first commit
This commit is contained in:
20
libs/web/identity/domain/README.md
Normal file
20
libs/web/identity/domain/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# web-identity-domain
|
||||
|
||||
Sesión y usuario en el navegador.
|
||||
|
||||
- **Import**: `@task-stream/web/identity/domain`
|
||||
- **Capa**: `domain` · **Tags**: `scope:web-identity`, `type:domain`
|
||||
|
||||
## Contenido
|
||||
|
||||
- `Session`, `CurrentUser`, `AuthConfig`, `isExpired`, `sessionFrom`.
|
||||
- Puertos: `AuthGateway`, `ProfileGateway`, `SessionStorage`, `AuthorizationFlow`.
|
||||
|
||||
## Comandos
|
||||
|
||||
```sh
|
||||
npx nx lint web-identity-domain
|
||||
npx nx test web-identity-domain # Jest
|
||||
```
|
||||
|
||||
Reglas de dependencia entre capas: [Arquitectura](../../../../docs/02-arquitectura.md#21-capas-hexagonales).
|
||||
3
libs/web/identity/domain/eslint.config.mjs
Normal file
3
libs/web/identity/domain/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from '../../../../eslint.config.mjs';
|
||||
|
||||
export default [...baseConfig];
|
||||
11
libs/web/identity/domain/jest.config.cts
Normal file
11
libs/web/identity/domain/jest.config.cts
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
displayName: 'web-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/web/identity/domain',
|
||||
};
|
||||
9
libs/web/identity/domain/project.json
Normal file
9
libs/web/identity/domain/project.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "web-identity-domain",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/web/identity/domain/src",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:web-identity", "type:domain"],
|
||||
"// targets": "to see all targets run: nx show project web-identity-domain --web",
|
||||
"targets": {}
|
||||
}
|
||||
2
libs/web/identity/domain/src/index.ts
Normal file
2
libs/web/identity/domain/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/session';
|
||||
export * from './lib/ports';
|
||||
41
libs/web/identity/domain/src/lib/ports.ts
Normal file
41
libs/web/identity/domain/src/lib/ports.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthConfig, CurrentUser, Session } from './session';
|
||||
|
||||
/** The API's sign-in endpoints (/api/auth/*). */
|
||||
export abstract class AuthGateway {
|
||||
abstract config(): Observable<AuthConfig | null>;
|
||||
abstract exchangeCode(
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
redirectUri: string,
|
||||
): Observable<Session>;
|
||||
abstract password(username: string, password: string): Observable<Session>;
|
||||
abstract refresh(refreshToken: string): Observable<Session>;
|
||||
}
|
||||
|
||||
/** Who the current token belongs to (GraphQL `me`). */
|
||||
export abstract class ProfileGateway {
|
||||
abstract me(accessToken: string): Observable<CurrentUser>;
|
||||
}
|
||||
|
||||
export abstract class SessionStorage {
|
||||
abstract load(): Session | null;
|
||||
abstract save(session: Session): void;
|
||||
abstract clear(): void;
|
||||
}
|
||||
|
||||
export interface AuthorizationResponse {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
/** Where the user wanted to go before signing in. */
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
/** Authorization Code + PKCE in the browser (redirect out and back). */
|
||||
export abstract class AuthorizationFlow {
|
||||
/** Leaves the app towards the identity server's authorize page. */
|
||||
abstract start(config: AuthConfig, returnUrl: string): Promise<void>;
|
||||
/** Validates `state` and returns what is needed to exchange the code. */
|
||||
abstract complete(params: URLSearchParams): AuthorizationResponse;
|
||||
}
|
||||
23
libs/web/identity/domain/src/lib/session.spec.ts
Normal file
23
libs/web/identity/domain/src/lib/session.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { isExpired, sessionFrom } from './session';
|
||||
|
||||
describe('session', () => {
|
||||
it('computes the expiry from expires_in', () => {
|
||||
const session = sessionFrom(
|
||||
{ accessToken: 't', refreshToken: 'r', expiresIn: 3600 },
|
||||
1_000,
|
||||
);
|
||||
expect(session).toEqual({
|
||||
accessToken: 't',
|
||||
refreshToken: 'r',
|
||||
expiresAt: 3_601_000,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats tokens about to expire as expired', () => {
|
||||
const session = sessionFrom({ accessToken: 't', expiresIn: 30 }, 0);
|
||||
expect(isExpired(session, 0)).toBe(true);
|
||||
expect(
|
||||
isExpired(sessionFrom({ accessToken: 't', expiresIn: 3600 }, 0), 0),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
46
libs/web/identity/domain/src/lib/session.ts
Normal file
46
libs/web/identity/domain/src/lib/session.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/** Tokens issued by Pireon (through the API's /api/auth/token proxy). */
|
||||
export interface Session {
|
||||
accessToken: string;
|
||||
refreshToken: string | null;
|
||||
/** Epoch milliseconds. */
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
appRoles: string[];
|
||||
}
|
||||
|
||||
/** How the browser can sign in (null config = the API has no Pireon client yet). */
|
||||
export interface AuthConfig {
|
||||
authorizeUrl: string;
|
||||
clientId: string;
|
||||
scopes: string[];
|
||||
passwordGrant: boolean;
|
||||
}
|
||||
|
||||
/** Refresh a little before the token actually expires. */
|
||||
export const EXPIRY_MARGIN_MS = 60_000;
|
||||
|
||||
export function isExpired(session: Session, now = Date.now()): boolean {
|
||||
return session.expiresAt - EXPIRY_MARGIN_MS <= now;
|
||||
}
|
||||
|
||||
export function sessionFrom(
|
||||
token: {
|
||||
accessToken: string;
|
||||
refreshToken?: string | null;
|
||||
expiresIn?: number | null;
|
||||
},
|
||||
now = Date.now(),
|
||||
): Session {
|
||||
// Personal access tokens are long-lived and come without expiry: one year.
|
||||
const expiresIn = token.expiresIn ?? 365 * 24 * 3600;
|
||||
return {
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken ?? null,
|
||||
expiresAt: now + expiresIn * 1000,
|
||||
};
|
||||
}
|
||||
23
libs/web/identity/domain/tsconfig.json
Normal file
23
libs/web/identity/domain/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
libs/web/identity/domain/tsconfig.lib.json
Normal file
15
libs/web/identity/domain/tsconfig.lib.json
Normal 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"
|
||||
]
|
||||
}
|
||||
16
libs/web/identity/domain/tsconfig.spec.json
Normal file
16
libs/web/identity/domain/tsconfig.spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user