import {Inject, Injectable} from '../di' import {EventBus} from '../event/EventBus' import {Awaitable, Maybe} from '../util' import {Authenticatable, AuthenticatableCredentials, AuthenticatableRepository} from './types' import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent' import {UserFlushedEvent} from './event/UserFlushedEvent' import {UserAuthenticationResumedEvent} from './event/UserAuthenticationResumedEvent' import {Logging} from '../service/Logging' /** * Base-class for a context that authenticates users and manages security. */ @Injectable() export abstract class SecurityContext { @Inject() protected readonly bus!: EventBus @Inject() protected readonly logging!: Logging /** The currently authenticated user, if one exists. */ private authenticatedUser?: Authenticatable constructor( /** The repository from which to draw users. */ 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.dispatch(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.dispatch(new UserAuthenticatedEvent(user, this)) } /** * Attempt to authenticate a user based on their credentials. * If the credentials are valid, the user will be authenticated, but the authentication * will not be persisted. That is, when the lifecycle ends, the user will be * unauthenticated implicitly. * @param credentials */ async attemptOnce(credentials: AuthenticatableCredentials): Promise> { const user = await this.repository.getByCredentials(credentials) if ( user ) { await this.authenticateOnce(user) return user } } /** * Attempt to authenticate a user based on their credentials. * If the credentials are valid, the user will be authenticated and the * authentication will be persisted. * @param credentials */ async attempt(credentials: AuthenticatableCredentials): Promise> { const user = await this.repository.getByCredentials(credentials) if ( user ) { await this.authenticate(user) return user } } /** * 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.dispatch(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.dispatch(new UserFlushedEvent(user, this)) } } /** * Assuming a user is still authenticated in the context, * try to look up and fill in the user. */ async resume(): Promise { const credentials = await this.getCredentials() this.logging.debug('resume:') this.logging.debug(credentials) const user = await this.repository.getByCredentials(credentials) if ( user ) { this.authenticatedUser = user await this.bus.dispatch(new UserAuthenticationResumedEvent(user, this)) } } /** * Write the current state of the security context to whatever storage * medium the context's host provides. */ abstract persist(): Awaitable /** * Get the credentials for the current user from whatever storage medium * the context's host provides. */ abstract getCredentials(): Awaitable /** * Get the currently authenticated user, if one exists. */ getUser(): Maybe { return this.authenticatedUser } /** * Returns true if there is a currently authenticated user. */ hasUser(): boolean { this.logging.debug('hasUser?') this.logging.debug(this.authenticatedUser) return Boolean(this.authenticatedUser) } }