lib/src/auth/context/SecurityContext.ts

123 lines
3.6 KiB
TypeScript

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<void> {} // 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<void> {
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<void> {
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<void> {
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<void> {
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.
*
* If there is NO USER to be resumed, then the method should flush
* the user from this context.
*/
abstract resume(): Awaitable<void>
/**
* Write the current state of the security context to whatever storage
* medium the context's host provides.
*/
abstract persist(): Awaitable<void>
/**
* Get the currently authenticated user, if one exists.
*/
getUser(): Maybe<Authenticatable> {
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)
}
}