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,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],
};
}
}