|
|
|
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<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.dispatch(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.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<Maybe<Authenticatable>> {
|
|
|
|
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<Maybe<Authenticatable>> {
|
|
|
|
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<void> {
|
|
|
|
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<void> {
|
|
|
|
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<void> {
|
|
|
|
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<void>
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the credentials for the current user from whatever storage medium
|
|
|
|
* the context's host provides.
|
|
|
|
*/
|
|
|
|
abstract getCredentials(): Awaitable<AuthenticatableCredentials>
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the currently authenticated user, if one exists.
|
|
|
|
*/
|
|
|
|
getUser(): Maybe<Authenticatable> {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|