Rework authentication system
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
bd7d6a2dbd
commit
5175d64e36
@ -1,20 +0,0 @@
|
||||
import {FormRequest, ValidationRules} from '../../forms'
|
||||
import {Is, Str} from '../../forms/rules/rules'
|
||||
import {Singleton} from '../../di'
|
||||
import {AuthenticatableCredentials} from '../types'
|
||||
|
||||
@Singleton()
|
||||
export class BasicLoginFormRequest extends FormRequest<AuthenticatableCredentials> {
|
||||
protected getRules(): ValidationRules {
|
||||
return {
|
||||
identifier: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
credential: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import {FormRequest, ValidationRules} from '../../forms'
|
||||
import {Is, Str} from '../../forms/rules/rules'
|
||||
import {Singleton} from '../../di'
|
||||
import {AuthenticatableCredentials} from '../types'
|
||||
|
||||
@Singleton()
|
||||
export class BasicRegisterFormRequest extends FormRequest<AuthenticatableCredentials> {
|
||||
protected getRules(): ValidationRules {
|
||||
return {
|
||||
identifier: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
Str.alphaNum,
|
||||
],
|
||||
credential: [
|
||||
Is.required,
|
||||
Str.lengthMin(8),
|
||||
Str.confirmed,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +1,41 @@
|
||||
import {Instantiable} from '../di'
|
||||
import {ORMUserRepository} from './orm/ORMUserRepository'
|
||||
import {OAuth2LoginConfig} from './external/oauth2/OAuth2LoginController'
|
||||
|
||||
/**
|
||||
* Inferface for type-checking the AuthenticatableRepositories values.
|
||||
*/
|
||||
export interface AuthenticatableRepositoryMapping {
|
||||
orm: Instantiable<ORMUserRepository>,
|
||||
}
|
||||
|
||||
/**
|
||||
* String mapping of AuthenticatableRepository implementations.
|
||||
*/
|
||||
export const AuthenticatableRepositories: AuthenticatableRepositoryMapping = {
|
||||
orm: ORMUserRepository,
|
||||
}
|
||||
import {Instantiable, isInstantiable} from '../di'
|
||||
import {AuthenticatableRepository} from './types'
|
||||
import {hasOwnProperty} from '../util'
|
||||
|
||||
/**
|
||||
* Interface for making the auth config type-safe.
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
repositories: {
|
||||
session: keyof AuthenticatableRepositoryMapping,
|
||||
},
|
||||
export interface AuthenticationConfig {
|
||||
storage: Instantiable<AuthenticatableRepository>,
|
||||
sources?: {
|
||||
[key: string]: OAuth2LoginConfig,
|
||||
[key: string]: Instantiable<AuthenticatableRepository>,
|
||||
},
|
||||
}
|
||||
|
||||
export function isAuthenticationConfig(what: unknown): what is AuthenticationConfig {
|
||||
if ( typeof what !== 'object' || !what ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( !hasOwnProperty(what, 'storage') || !hasOwnProperty(what, 'sources') ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( !isInstantiable(what.storage) || !(what.storage.prototype instanceof AuthenticatableRepository) ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( typeof what.sources !== 'object' ) {
|
||||
return false
|
||||
}
|
||||
|
||||
for ( const key in what.sources ) {
|
||||
if ( !hasOwnProperty(what.sources, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const source = what.sources[key]
|
||||
if ( !isInstantiable(source) || !(source.prototype instanceof AuthenticatableRepository) ) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
import {SecurityContext} from './SecurityContext'
|
||||
import {Inject, Injectable} from '../../di'
|
||||
import {Session} from '../../http/session/Session'
|
||||
import {Awaitable} from '../../util'
|
||||
import {AuthenticatableRepository} from '../types'
|
||||
import {UserAuthenticationResumedEvent} from '../event/UserAuthenticationResumedEvent'
|
||||
|
||||
export const EXTOLLO_AUTH_SESSION_KEY = '@extollo:auth.securityIdentifier'
|
||||
|
||||
/**
|
||||
* Security context implementation that uses the session as storage.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionSecurityContext extends SecurityContext {
|
||||
@Inject()
|
||||
protected readonly session!: Session
|
||||
|
||||
constructor(
|
||||
/** The repository from which to draw users. */
|
||||
public readonly repository: AuthenticatableRepository,
|
||||
) {
|
||||
super(repository, 'session')
|
||||
}
|
||||
|
||||
persist(): Awaitable<void> {
|
||||
this.session.set(EXTOLLO_AUTH_SESSION_KEY, this.getUser()?.getIdentifier())
|
||||
}
|
||||
|
||||
async resume(): Promise<void> {
|
||||
const identifier = this.session.get(EXTOLLO_AUTH_SESSION_KEY)
|
||||
if ( identifier ) {
|
||||
const user = await this.repository.getByIdentifier(identifier)
|
||||
if ( user ) {
|
||||
this.authenticatedUser = user
|
||||
await this.bus.dispatch(new UserAuthenticationResumedEvent(user, this))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import {SecurityContext} from '../SecurityContext'
|
||||
import {Inject, Injectable} from '../../di'
|
||||
import {Session} from '../../http/session/Session'
|
||||
import {Awaitable} from '../../util'
|
||||
import {AuthenticatableCredentials, AuthenticatableRepository} from '../types'
|
||||
|
||||
/**
|
||||
* Security context implementation that uses the session as storage.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionSecurityContext extends SecurityContext {
|
||||
@Inject()
|
||||
protected readonly session!: Session
|
||||
|
||||
constructor(
|
||||
/** The repository from which to draw users. */
|
||||
public readonly repository: AuthenticatableRepository,
|
||||
) {
|
||||
super(repository, 'session')
|
||||
}
|
||||
|
||||
getCredentials(): Awaitable<AuthenticatableCredentials> {
|
||||
return {
|
||||
identifier: '',
|
||||
credential: this.session.get('extollo.auth.securityIdentifier'),
|
||||
}
|
||||
}
|
||||
|
||||
persist(): Awaitable<void> {
|
||||
this.session.set('extollo.auth.securityIdentifier', this.getUser()?.getIdentifier())
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import {Event} from '../../event/Event'
|
||||
import {SecurityContext} from '../context/SecurityContext'
|
||||
import {Awaitable, JSONState} from '../../util'
|
||||
import {Authenticatable} from '../types'
|
||||
|
||||
/**
|
||||
* Event fired when a user is authenticated.
|
||||
*/
|
||||
export class AuthenticationEvent extends Event {
|
||||
constructor(
|
||||
public readonly user: Authenticatable,
|
||||
public readonly context: SecurityContext,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
user: await this.user.dehydrate(),
|
||||
contextName: this.context.name,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// TODO fill this in
|
||||
}
|
||||
}
|
@ -1,27 +1,6 @@
|
||||
import {Event} from '../../event/Event'
|
||||
import {SecurityContext} from '../SecurityContext'
|
||||
import {Awaitable, JSONState} from '../../util'
|
||||
import {Authenticatable} from '../types'
|
||||
import {AuthenticationEvent} from './AuthenticationEvent'
|
||||
|
||||
/**
|
||||
* Event fired when a user is authenticated.
|
||||
*/
|
||||
export class UserAuthenticatedEvent extends Event {
|
||||
constructor(
|
||||
public readonly user: Authenticatable,
|
||||
public readonly context: SecurityContext,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
user: await this.user.dehydrate(),
|
||||
contextName: this.context.name,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// TODO fill this in
|
||||
}
|
||||
}
|
||||
export class UserAuthenticatedEvent extends AuthenticationEvent {}
|
||||
|
@ -1,27 +1,6 @@
|
||||
import {Event} from '../../event/Event'
|
||||
import {SecurityContext} from '../SecurityContext'
|
||||
import {Awaitable, JSONState} from '../../util'
|
||||
import {Authenticatable} from '../types'
|
||||
import {AuthenticationEvent} from './AuthenticationEvent'
|
||||
|
||||
/**
|
||||
* Event fired when a security context for a given user is resumed.
|
||||
* Event raised when a user is re-authenticated to a security context
|
||||
*/
|
||||
export class UserAuthenticationResumedEvent extends Event {
|
||||
constructor(
|
||||
public readonly user: Authenticatable,
|
||||
public readonly context: SecurityContext,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
user: await this.user.dehydrate(),
|
||||
contextName: this.context.name,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// TODO fill this in
|
||||
}
|
||||
}
|
||||
export class UserAuthenticationResumedEvent extends AuthenticationEvent {}
|
||||
|
@ -1,27 +1,6 @@
|
||||
import {Event} from '../../event/Event'
|
||||
import {SecurityContext} from '../SecurityContext'
|
||||
import {Awaitable, JSONState} from '../../util'
|
||||
import {Authenticatable} from '../types'
|
||||
import {AuthenticationEvent} from './AuthenticationEvent'
|
||||
|
||||
/**
|
||||
* Event fired when a user is unauthenticated.
|
||||
*/
|
||||
export class UserFlushedEvent extends Event {
|
||||
constructor(
|
||||
public readonly user: Authenticatable,
|
||||
public readonly context: SecurityContext,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
user: await this.user.dehydrate(),
|
||||
contextName: this.context.name,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// TODO fill this in
|
||||
}
|
||||
}
|
||||
export class UserFlushedEvent extends AuthenticationEvent {}
|
||||
|
@ -1,95 +0,0 @@
|
||||
import {Controller} from '../../../http/Controller'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {Config} from '../../../service/Config'
|
||||
import {Request} from '../../../http/lifecycle/Request'
|
||||
import {ResponseObject, Route} from '../../../http/routing/Route'
|
||||
import {ErrorWithContext} from '../../../util'
|
||||
import {OAuth2Repository} from './OAuth2Repository'
|
||||
import {json} from '../../../http/response/JSONResponseFactory'
|
||||
|
||||
export interface OAuth2LoginConfig {
|
||||
name: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
redirectUrl: string,
|
||||
authorizationCodeField: string,
|
||||
tokenEndpoint: string,
|
||||
tokenEndpointMapping?: {
|
||||
clientId?: string,
|
||||
clientSecret?: string,
|
||||
grantType?: string,
|
||||
codeKey?: string,
|
||||
},
|
||||
tokenEndpointResponseMapping?: {
|
||||
token?: string,
|
||||
expiresIn?: string,
|
||||
expiresAt?: string,
|
||||
},
|
||||
userEndpoint: string,
|
||||
userEndpointResponseMapping?: {
|
||||
identifier?: string,
|
||||
display?: string,
|
||||
},
|
||||
}
|
||||
|
||||
export function isOAuth2LoginConfig(what: unknown): what is OAuth2LoginConfig {
|
||||
return (
|
||||
Boolean(what)
|
||||
&& typeof (what as any).name === 'string'
|
||||
&& typeof (what as any).clientId === 'string'
|
||||
&& typeof (what as any).clientSecret === 'string'
|
||||
&& typeof (what as any).redirectUrl === 'string'
|
||||
&& typeof (what as any).authorizationCodeField === 'string'
|
||||
&& typeof (what as any).tokenEndpoint === 'string'
|
||||
&& typeof (what as any).userEndpoint === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2LoginController extends Controller {
|
||||
public static routes(configName: string): void {
|
||||
Route.group(`/auth/${configName}`, () => {
|
||||
Route.get('login', (request: Request) => {
|
||||
const controller = <OAuth2LoginController> request.make(OAuth2LoginController, configName)
|
||||
return controller.getLogin()
|
||||
}).pre('@auth:guest')
|
||||
}).pre('@auth:web')
|
||||
}
|
||||
|
||||
@Inject()
|
||||
protected readonly config!: Config
|
||||
|
||||
constructor(
|
||||
protected readonly request: Request,
|
||||
protected readonly configName: string,
|
||||
) {
|
||||
super(request)
|
||||
}
|
||||
|
||||
public async getLogin(): Promise<ResponseObject> {
|
||||
const repo = this.getRepository()
|
||||
if ( repo.shouldRedirect() ) {
|
||||
return repo.redirect()
|
||||
}
|
||||
|
||||
// We were redirected from the auth source
|
||||
const user = await repo.redeem()
|
||||
return json(user)
|
||||
}
|
||||
|
||||
protected getRepository(): OAuth2Repository {
|
||||
return this.request.make(OAuth2Repository, this.getConfig())
|
||||
}
|
||||
|
||||
protected getConfig(): OAuth2LoginConfig {
|
||||
const config = this.config.get(`auth.sources.${this.configName}`)
|
||||
if ( !isOAuth2LoginConfig(config) ) {
|
||||
throw new ErrorWithContext('Invalid OAuth2 source config.', {
|
||||
configName: this.configName,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
import {
|
||||
Authenticatable,
|
||||
AuthenticatableCredentials,
|
||||
AuthenticatableRepository,
|
||||
} from '../../types'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {
|
||||
Awaitable,
|
||||
dataGetUnsafe,
|
||||
fetch,
|
||||
Maybe,
|
||||
MethodNotSupportedError,
|
||||
UniversalPath,
|
||||
universalPath,
|
||||
uuid4,
|
||||
} from '../../../util'
|
||||
import {OAuth2LoginConfig} from './OAuth2LoginController'
|
||||
import {Session} from '../../../http/session/Session'
|
||||
import {ResponseObject} from '../../../http/routing/Route'
|
||||
import {temporary} from '../../../http/response/TemporaryRedirectResponseFactory'
|
||||
import {Request} from '../../../http/lifecycle/Request'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
import {OAuth2User} from './OAuth2User'
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2Repository implements AuthenticatableRepository {
|
||||
@Inject()
|
||||
protected readonly session!: Session
|
||||
|
||||
@Inject()
|
||||
protected readonly request!: Request
|
||||
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
constructor(
|
||||
protected readonly config: OAuth2LoginConfig,
|
||||
) { }
|
||||
|
||||
public createByCredentials(): Awaitable<Authenticatable> {
|
||||
throw new MethodNotSupportedError()
|
||||
}
|
||||
|
||||
getByCredentials(credentials: AuthenticatableCredentials): Awaitable<Maybe<Authenticatable>> {
|
||||
return this.getAuthenticatableFromBearer(credentials.credential)
|
||||
}
|
||||
|
||||
getByIdentifier(): Awaitable<Maybe<Authenticatable>> {
|
||||
return undefined
|
||||
}
|
||||
|
||||
public getRedirectUrl(state?: string): UniversalPath {
|
||||
const url = universalPath(this.config.redirectUrl)
|
||||
if ( state ) {
|
||||
url.query.append('state', state)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
public getTokenEndpoint(): UniversalPath {
|
||||
return universalPath(this.config.tokenEndpoint)
|
||||
}
|
||||
|
||||
public getUserEndpoint(): UniversalPath {
|
||||
return universalPath(this.config.userEndpoint)
|
||||
}
|
||||
|
||||
public async redeem(): Promise<Maybe<OAuth2User>> {
|
||||
if ( !this.stateIsValid() ) {
|
||||
return // FIXME throw
|
||||
}
|
||||
|
||||
const body = new URLSearchParams()
|
||||
|
||||
if ( this.config.tokenEndpointMapping ) {
|
||||
if ( this.config.tokenEndpointMapping.clientId ) {
|
||||
body.append(this.config.tokenEndpointMapping.clientId, this.config.clientId)
|
||||
}
|
||||
|
||||
if ( this.config.tokenEndpointMapping.clientSecret ) {
|
||||
body.append(this.config.tokenEndpointMapping.clientSecret, this.config.clientSecret)
|
||||
}
|
||||
|
||||
if ( this.config.tokenEndpointMapping.codeKey ) {
|
||||
body.append(this.config.tokenEndpointMapping.codeKey, String(this.request.input(this.config.authorizationCodeField)))
|
||||
}
|
||||
|
||||
if ( this.config.tokenEndpointMapping.grantType ) {
|
||||
body.append(this.config.tokenEndpointMapping.grantType, 'authorization_code')
|
||||
}
|
||||
}
|
||||
|
||||
this.logging.debug(`Redeeming auth code: ${body.toString()}`)
|
||||
|
||||
const response = await fetch(this.getTokenEndpoint().toRemote, {
|
||||
method: 'post',
|
||||
body: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if ( typeof data !== 'object' || data === null ) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
this.logging.debug(data)
|
||||
const bearer = String(dataGetUnsafe(data, this.config.tokenEndpointResponseMapping?.token ?? 'bearer'))
|
||||
|
||||
this.logging.debug(bearer)
|
||||
if ( !bearer || typeof bearer !== 'string' ) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
return this.getAuthenticatableFromBearer(bearer)
|
||||
}
|
||||
|
||||
public async getAuthenticatableFromBearer(bearer: string): Promise<Maybe<OAuth2User>> {
|
||||
const response = await fetch(this.getUserEndpoint().toRemote, {
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${bearer}`,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if ( typeof data !== 'object' || data === null ) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
return new OAuth2User(data, this.config)
|
||||
}
|
||||
|
||||
public stateIsValid(): boolean {
|
||||
const correctState = this.session.get('extollo.auth.oauth2.state', '')
|
||||
const inputState = this.request.input('state') || ''
|
||||
return correctState === inputState
|
||||
}
|
||||
|
||||
public shouldRedirect(): boolean {
|
||||
const codeField = this.config.authorizationCodeField
|
||||
const code = this.request.input(codeField)
|
||||
return !code
|
||||
}
|
||||
|
||||
public async redirect(): Promise<ResponseObject> {
|
||||
const state = uuid4()
|
||||
await this.session.set('extollo.auth.oauth2.state', state)
|
||||
return temporary(this.getRedirectUrl(state).toRemote)
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import {Authenticatable, AuthenticatableIdentifier} from '../../types'
|
||||
import {OAuth2LoginConfig} from './OAuth2LoginController'
|
||||
import {Awaitable, dataGetUnsafe, InvalidJSONStateError, JSONState} from '../../../util'
|
||||
|
||||
export class OAuth2User implements Authenticatable {
|
||||
protected displayField: string
|
||||
|
||||
protected identifierField: string
|
||||
|
||||
constructor(
|
||||
protected data: {[key: string]: any},
|
||||
config: OAuth2LoginConfig,
|
||||
) {
|
||||
this.displayField = config.userEndpointResponseMapping?.display || 'name'
|
||||
this.identifierField = config.userEndpointResponseMapping?.identifier || 'id'
|
||||
}
|
||||
|
||||
getDisplayIdentifier(): string {
|
||||
return String(dataGetUnsafe(this.data, this.displayField || 'name', ''))
|
||||
}
|
||||
|
||||
getIdentifier(): AuthenticatableIdentifier {
|
||||
return String(dataGetUnsafe(this.data, this.identifierField || 'id', ''))
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
isOAuth2User: true,
|
||||
data: this.data,
|
||||
displayField: this.displayField,
|
||||
identifierField: this.identifierField,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): Awaitable<void> {
|
||||
if (
|
||||
!state.isOAuth2User
|
||||
|| typeof state.data !== 'object'
|
||||
|| state.data === null
|
||||
|| typeof state.displayField !== 'string'
|
||||
|| typeof state.identifierField !== 'string'
|
||||
) {
|
||||
throw new InvalidJSONStateError('OAuth2User state is invalid', { state })
|
||||
}
|
||||
|
||||
this.data = state.data
|
||||
this.identifierField = state.identifierField
|
||||
this.displayField = state.identifierField
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import {
|
||||
Authenticatable,
|
||||
AuthenticatableCredentials,
|
||||
AuthenticatableIdentifier,
|
||||
AuthenticatableRepository,
|
||||
} from '../types'
|
||||
import {Awaitable, Maybe} from '../../util'
|
||||
import {ORMUser} from './ORMUser'
|
||||
import {Container, Inject, Injectable} from '../../di'
|
||||
import {AuthenticatableAlreadyExistsError} from '../AuthenticatableAlreadyExistsError'
|
||||
|
||||
/**
|
||||
* A user repository implementation that looks up users stored in the database.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ORMUserRepository extends AuthenticatableRepository {
|
||||
@Inject('injector')
|
||||
protected readonly injector!: Container
|
||||
|
||||
/** Look up the user by their username. */
|
||||
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
||||
return ORMUser.query<ORMUser>()
|
||||
.where('username', '=', id)
|
||||
.first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to look up a user by the credentials provided.
|
||||
* If a securityIdentifier is specified, look up the user by username.
|
||||
* If username/password are specified, look up the user and verify the password.
|
||||
* @param credentials
|
||||
*/
|
||||
async getByCredentials(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
||||
if ( !credentials.identifier && credentials.credential ) {
|
||||
return ORMUser.query<ORMUser>()
|
||||
.where('username', '=', credentials.credential)
|
||||
.first()
|
||||
}
|
||||
|
||||
if ( credentials.identifier && credentials.credential ) {
|
||||
const user = await ORMUser.query<ORMUser>()
|
||||
.where('username', '=', credentials.identifier)
|
||||
.first()
|
||||
|
||||
if ( user && await user.verifyPassword(credentials.credential) ) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createByCredentials(credentials: AuthenticatableCredentials): Promise<Authenticatable> {
|
||||
if ( await this.getByCredentials(credentials) ) {
|
||||
throw new AuthenticatableAlreadyExistsError(`Authenticatable already exists with credentials.`, {
|
||||
identifier: credentials.identifier,
|
||||
})
|
||||
}
|
||||
|
||||
const user = <ORMUser> this.injector.make(ORMUser)
|
||||
user.username = credentials.identifier
|
||||
await user.setPassword(credentials.credential)
|
||||
await user.save()
|
||||
|
||||
return user
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import {
|
||||
Authenticatable,
|
||||
AuthenticatableIdentifier,
|
||||
AuthenticatableRepository,
|
||||
} from '../../types'
|
||||
import {Awaitable, Maybe, uuid4} from '../../../util'
|
||||
import {ORMUser} from './ORMUser'
|
||||
import {Container, Inject, Injectable} from '../../../di'
|
||||
import {AuthenticatableAlreadyExistsError} from '../../AuthenticatableAlreadyExistsError'
|
||||
|
||||
/**
|
||||
* A user repository implementation that looks up users stored in the database.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ORMUserRepository extends AuthenticatableRepository {
|
||||
@Inject('injector')
|
||||
protected readonly injector!: Container
|
||||
|
||||
/** Look up the user by their username. */
|
||||
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
||||
return (this.injector.getStaticOverride(ORMUser) as typeof ORMUser).query<ORMUser>()
|
||||
.where('username', '=', id)
|
||||
.first()
|
||||
}
|
||||
|
||||
/** Returns true if this repository supports registering users. */
|
||||
supportsRegistration(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/** Create a user in this repository from basic credentials. */
|
||||
async createFromCredentials(username: string, password: string): Promise<Authenticatable> {
|
||||
if ( await this.getByIdentifier(username) ) {
|
||||
throw new AuthenticatableAlreadyExistsError(`Authenticatable already exists with credentials.`, {
|
||||
username,
|
||||
})
|
||||
}
|
||||
|
||||
const user = <ORMUser> this.injector.makeByStaticOverride(ORMUser)
|
||||
user.username = username
|
||||
await user.setPassword(password)
|
||||
await user.save()
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/** Create a user in this repository from an external Authenticatable instance. */
|
||||
async createFromExternal(user: Authenticatable): Promise<Authenticatable> {
|
||||
return this.createFromCredentials(String(user.getUniqueIdentifier()), uuid4())
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import {FormRequest, ValidationRules} from '../../../forms'
|
||||
import {Is, Str} from '../../../forms/rules/rules'
|
||||
import {Singleton} from '../../../di'
|
||||
|
||||
export interface BasicLoginFormData {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
@Singleton()
|
||||
export class BasicLoginFormRequest extends FormRequest<BasicLoginFormData> {
|
||||
protected getRules(): ValidationRules {
|
||||
return {
|
||||
username: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
password: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import {FormRequest, ValidationRules} from '../../../forms'
|
||||
import {Is, Str} from '../../../forms/rules/rules'
|
||||
import {Singleton} from '../../../di'
|
||||
|
||||
export interface BasicRegisterFormData {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
@Singleton()
|
||||
export class BasicRegisterFormRequest extends FormRequest<BasicRegisterFormData> {
|
||||
protected getRules(): ValidationRules {
|
||||
return {
|
||||
username: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
password: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
Str.confirmed,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
html
|
||||
head
|
||||
meta(http-equiv='Refresh' content='0; url="' + redirectUrl)
|
||||
title Redirecting...
|
||||
body
|
||||
script.
|
||||
var url = `${redirectUrl}`
|
||||
window.location.assign(url)
|
Loading…
Reference in new issue