Start auth provider system
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Garrett Mills 2022-01-20 00:55:21 -06:00
parent cfd555723b
commit 506fb55c74
8 changed files with 181 additions and 7 deletions

View File

@ -1,11 +1,12 @@
import {Instantiable, isInstantiable} from '../di' import {Instantiable, isInstantiable} from '../di'
import {AuthenticatableRepository} from './types' import {AuthenticatableRepository} from './types'
import {hasOwnProperty} from '../util' import {hasOwnProperty} from '../util'
import {LoginProviderConfig} from './provider/LoginProvider'
export interface AuthenticationConfig { export interface AuthenticationConfig {
storage: Instantiable<AuthenticatableRepository>, storage: Instantiable<AuthenticatableRepository>,
sources?: { providers?: {
[key: string]: Instantiable<AuthenticatableRepository>, [key: string]: LoginProviderConfig
}, },
} }

View File

@ -0,0 +1,61 @@
import {Request} from '../../http/lifecycle/Request'
import {ResponseObject, Route} from '../../http/routing/Route'
import {GuestRequiredMiddleware} from '../middleware/GuestRequiredMiddleware'
import {AuthRequiredMiddleware} from '../middleware/AuthRequiredMiddleware'
import {Inject, Injectable} from '../../di'
import {SecurityContext} from '../context/SecurityContext'
import {redirect} from '../../http/response/RedirectResponseFactory'
export interface LoginProviderConfig {
default: boolean,
allow?: {
login?: boolean,
registration?: boolean,
},
}
@Injectable()
export abstract class LoginProvider<TConfig extends LoginProviderConfig> {
@Inject()
protected readonly security!: SecurityContext
constructor(
protected name: string,
protected config: TConfig,
) {}
public routes(): void {
Route.get('login')
.alias(`@auth:${this.name}:login`)
.pipe(line => line.when(this.config.default, route => route.alias('@auth:login')))
.pre(GuestRequiredMiddleware)
.passingRequest()
.handledBy(this.login.bind(this))
Route.any('logout')
.alias(`@auth:${this.name}:logout`)
.pipe(line => line.when(this.config.default, route => route.alias('@auth:logout')))
.pre(AuthRequiredMiddleware)
.passingRequest()
.handledBy(this.logout.bind(this))
Route.get('register')
.alias(`@auth:${this.name}:register`)
.pipe(line => line.when(this.config.default, route => route.alias('@auth:register')))
.pre(GuestRequiredMiddleware)
.passingRequest()
.handledBy(this.registration.bind(this))
}
public abstract login(request: Request): ResponseObject
public abstract logout(request: Request): ResponseObject
public registration(request: Request): ResponseObject {
return this.login(request)
}
protected redirectToIntendedRoute(): ResponseObject {
return redirect('/') // FIXME
}
}

View File

@ -0,0 +1,8 @@
import { z } from 'zod'
export type BasicLoginAttempt = z.infer<typeof BasicLoginAttemptType>
export const BasicLoginAttemptType = z.object({
username: z.string().nonempty(),
password: z.string().nonempty(),
})

View File

@ -0,0 +1,75 @@
import {LoginProvider, LoginProviderConfig} from '../LoginProvider'
import {ResponseObject, Route} from '../../../http/routing/Route'
import {view} from '../../../http/response/ViewResponseFactory'
import {Valid, Validator} from '../../../validation/Validator'
import {BasicLoginAttempt, BasicLoginAttemptType} from './BasicLoginAttempt'
import {BasicRegistrationAttempt, BasicRegistrationAttemptType} from './BasicRegistrationAttempt'
/**
* LoginProvider implementation that provides basic username/password login.
*/
export class BasicLoginProvider extends LoginProvider<LoginProviderConfig> {
public routes(): void {
super.routes()
Route.post('/login')
.alias(`@auth:${this.name}:login.submit`)
.input(Validator.fromSchema<BasicLoginAttempt>(BasicLoginAttemptType))
.handledBy((...p) => this.attemptLogin(...p))
Route.post('/register')
.alias(`@auth:${this.name}:register.submit`)
.input(Validator.fromSchema<BasicRegistrationAttempt>(BasicRegistrationAttemptType))
.handledBy((...p) => this.attemptRegistration(...p))
}
public login(): ResponseObject {
return view('@extollo:auth:login')
}
public async logout(): Promise<ResponseObject> {
await this.security.flush()
return view('@extollo:auth:logout')
}
public registration(): ResponseObject {
return view('@extollo:auth:register')
}
/** Attempt to authenticate the user with a username/password. */
public async attemptLogin(attempt: Valid<BasicLoginAttempt>): Promise<ResponseObject> {
const user = await this.security.repository.getByIdentifier(attempt.username)
if ( !user ) {
throw new Error('TODO')
}
if ( !(await user.validateCredential(attempt.password)) ) {
throw new Error('TODO')
}
await this.security.authenticate(user)
return this.redirectToIntendedRoute()
}
/** Attempt to register the user with a username/password. */
public async attemptRegistration(attempt: Valid<BasicRegistrationAttempt>): Promise<ResponseObject> {
const existingUser = await this.security.repository.getByIdentifier(attempt.username)
if ( existingUser ) {
throw new Error('TODO')
}
if ( attempt.password !== attempt.passwordConfirmation ) {
throw new Error('TODO')
}
const user = await this.security.repository.createFromCredentials(attempt.username, attempt.password)
;(user as any).firstName = attempt.firstName
;(user as any).lastName = attempt.lastName
if ( typeof (user as any).save === 'function' ) {
await (user as any).save()
}
await this.security.authenticate(user)
return this.redirectToIntendedRoute()
}
}

View File

@ -0,0 +1,19 @@
import { z } from 'zod'
export type BasicRegistrationAttempt = z.infer<typeof BasicRegistrationAttemptType>
export const BasicRegistrationAttemptType = z.object({
firstName: z.string().nonempty(),
lastName: z.string().nonempty(),
username: z.string().nonempty(),
password: z.string()
.nonempty()
.min(8),
passwordConfirmation: z.string()
.nonempty()
.min(8),
})

View File

@ -15,8 +15,6 @@ block form
input#inputPassword.form-control(type='password' name='password' required placeholder='Password') input#inputPassword.form-control(type='password' name='password' required placeholder='Password')
label(for='inputPassword') Password label(for='inputPassword') Password
button.btn.btn-lg.btn-primary.btn-block.btn-login.text-uppercase.font-weight-bold.mb-2.form-submit-button(type='submit') Login button.btn.btn-lg.btn-primary.btn-block.btn-login.text-uppercase.font-weight-bold.mb-2.form-submit-button(type='submit') Login
.text-center .text-center

View File

@ -8,7 +8,7 @@ import {Queueable} from '../support/queue/Queue'
*/ */
@Singleton() @Singleton()
export class Queueables extends CanonicalStatic<Queueable, Instantiable<Queueable>> { export class Queueables extends CanonicalStatic<Queueable, Instantiable<Queueable>> {
protected appPath = ['queueables'] protected appPath = ['jobs']
protected canonicalItem = 'job' protected canonicalItem = 'job'

View File

@ -1,4 +1,4 @@
import { z } from 'zod' import {z, ZodType} from 'zod'
import {InjectionAware} from '../di' import {InjectionAware} from '../di'
import {ErrorWithContext, TypeTag} from '../util' import {ErrorWithContext, TypeTag} from '../util'
import {ZodifyRecipient} from './ZodifyRecipient' import {ZodifyRecipient} from './ZodifyRecipient'
@ -8,7 +8,7 @@ import {Logging} from '../service/Logging'
/** Type tag for a validated runtime type. */ /** Type tag for a validated runtime type. */
export type Valid<T> = TypeTag<'@extollo/lib:Valid'> & T export type Valid<T> = TypeTag<'@extollo/lib:Valid'> & T
export type ValidatorFactory<T extends Validator<T>> = T | (() => T) export type ValidatorFactory<T> = Validator<T> | (() => Validator<T>)
/** /**
* Error thrown if the schema for a validator cannot be located. * Error thrown if the schema for a validator cannot be located.
@ -45,8 +45,16 @@ export class ValidationError<T> extends ErrorWithContext {
* Validates input data against a schema at runtime. * Validates input data against a schema at runtime.
*/ */
export class Validator<T> extends InjectionAware implements ZodifyRecipient { export class Validator<T> extends InjectionAware implements ZodifyRecipient {
public static fromSchema<T2>(type: ZodType<T2>): Validator<T2> {
const inst = new Validator<T2>()
inst.schema = type
return inst
}
__exZodifiedSchemata: number[] = [] __exZodifiedSchemata: number[] = []
protected schema?: ZodType<T>
/** /**
* Parse the input data against the schema. * Parse the input data against the schema.
* @throws ValidationError * @throws ValidationError
@ -90,6 +98,10 @@ export class Validator<T> extends InjectionAware implements ZodifyRecipient {
/** Get the Zod schema. */ /** Get the Zod schema. */
protected getZod(): z.ZodType<T> { protected getZod(): z.ZodType<T> {
if ( this.schema ) {
return this.schema
}
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
if ( this.__exZodifiedSchemata.length < 1 ) { if ( this.__exZodifiedSchemata.length < 1 ) {
throw new InvalidSchemaMappingError() throw new InvalidSchemaMappingError()