import {Inject, Injectable} from '../../di' import {Awaitable, HTTPStatus, Maybe} from '../../util' import {Authenticatable, AuthenticatableRepository} from '../types' import {Logging} from '../../service/Logging' import {UserAuthenticatedEvent} from '../event/UserAuthenticatedEvent' import {UserFlushedEvent} from '../event/UserFlushedEvent' import {Bus} from '../../support/bus' import {HTTPError} from '../../http/HTTPError' /** * Base-class for a context that authenticates users and manages security. */ @Injectable() export abstract class SecurityContext { @Inject() protected readonly bus!: Bus @Inject() protected readonly logging!: Logging /** The currently authenticated user, if one exists. */ protected authenticatedUser?: Authenticatable constructor( /** The repository where users are persisted. */ public readonly repository: AuthenticatableRepository, /** The name of this context. */ public readonly name: string, ) { } /** * Called when the context is created. Can be used by child-classes to do setup work. */ initialize(): Awaitable {} // eslint-disable-line @typescript-eslint/no-empty-function /** * Authenticate the given user, without persisting the authentication. * That is, when the lifecycle ends, the user will be unauthenticated implicitly. * @param user */ async authenticateOnce(user: Authenticatable): Promise { this.authenticatedUser = user await this.bus.push(new UserAuthenticatedEvent(user, this)) } /** * Authenticate the given user and persist the authentication. * @param user */ async authenticate(user: Authenticatable): Promise { this.authenticatedUser = user await this.persist() await this.bus.push(new UserAuthenticatedEvent(user, this)) } /** * Unauthenticate the current user, if one exists, but do not persist the change. */ async flushOnce(): Promise { const user = this.authenticatedUser if ( user ) { this.authenticatedUser = undefined await this.bus.push(new UserFlushedEvent(user, this)) } } /** * Unauthenticate the current user, if one exists, and persist the change. */ async flush(): Promise { const user = this.authenticatedUser if ( user ) { this.authenticatedUser = undefined await this.persist() await this.bus.push(new UserFlushedEvent(user, this)) } } /** * Assuming a user is still authenticated in the context, * try to look up and fill in the user. */ abstract resume(): Awaitable /** * Write the current state of the security context to whatever storage * medium the context's host provides. */ abstract persist(): Awaitable /** * Get the currently authenticated user, if one exists. */ getUser(): Maybe { return this.authenticatedUser } /** Get the current user or throw an authorization error. */ user(): Authenticatable { if ( !this.hasUser() ) { throw new HTTPError(HTTPStatus.UNAUTHORIZED) } const user = this.getUser() if ( !user ) { throw new HTTPError(HTTPStatus.UNAUTHORIZED) } return user } /** * Returns true if there is a currently authenticated user. */ hasUser(): boolean { return Boolean(this.authenticatedUser) } }