This commit is contained in:
parent
cfd555723b
commit
506fb55c74
@ -1,11 +1,12 @@
|
||||
import {Instantiable, isInstantiable} from '../di'
|
||||
import {AuthenticatableRepository} from './types'
|
||||
import {hasOwnProperty} from '../util'
|
||||
import {LoginProviderConfig} from './provider/LoginProvider'
|
||||
|
||||
export interface AuthenticationConfig {
|
||||
storage: Instantiable<AuthenticatableRepository>,
|
||||
sources?: {
|
||||
[key: string]: Instantiable<AuthenticatableRepository>,
|
||||
providers?: {
|
||||
[key: string]: LoginProviderConfig
|
||||
},
|
||||
}
|
||||
|
||||
|
61
src/auth/provider/LoginProvider.ts
Normal file
61
src/auth/provider/LoginProvider.ts
Normal 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
|
||||
}
|
||||
}
|
8
src/auth/provider/basic/BasicLoginAttempt.ts
Normal file
8
src/auth/provider/basic/BasicLoginAttempt.ts
Normal 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(),
|
||||
})
|
75
src/auth/provider/basic/BasicLoginProvider.ts
Normal file
75
src/auth/provider/basic/BasicLoginProvider.ts
Normal 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()
|
||||
}
|
||||
}
|
19
src/auth/provider/basic/BasicRegistrationAttempt.ts
Normal file
19
src/auth/provider/basic/BasicRegistrationAttempt.ts
Normal 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),
|
||||
})
|
@ -15,8 +15,6 @@ block form
|
||||
input#inputPassword.form-control(type='password' name='password' required placeholder='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
|
||||
|
||||
.text-center
|
||||
|
@ -8,7 +8,7 @@ import {Queueable} from '../support/queue/Queue'
|
||||
*/
|
||||
@Singleton()
|
||||
export class Queueables extends CanonicalStatic<Queueable, Instantiable<Queueable>> {
|
||||
protected appPath = ['queueables']
|
||||
protected appPath = ['jobs']
|
||||
|
||||
protected canonicalItem = 'job'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { z } from 'zod'
|
||||
import {z, ZodType} from 'zod'
|
||||
import {InjectionAware} from '../di'
|
||||
import {ErrorWithContext, TypeTag} from '../util'
|
||||
import {ZodifyRecipient} from './ZodifyRecipient'
|
||||
@ -8,7 +8,7 @@ import {Logging} from '../service/Logging'
|
||||
/** Type tag for a validated runtime type. */
|
||||
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.
|
||||
@ -45,8 +45,16 @@ export class ValidationError<T> extends ErrorWithContext {
|
||||
* Validates input data against a schema at runtime.
|
||||
*/
|
||||
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[] = []
|
||||
|
||||
protected schema?: ZodType<T>
|
||||
|
||||
/**
|
||||
* Parse the input data against the schema.
|
||||
* @throws ValidationError
|
||||
@ -90,6 +98,10 @@ export class Validator<T> extends InjectionAware implements ZodifyRecipient {
|
||||
|
||||
/** Get the Zod schema. */
|
||||
protected getZod(): z.ZodType<T> {
|
||||
if ( this.schema ) {
|
||||
return this.schema
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if ( this.__exZodifiedSchemata.length < 1 ) {
|
||||
throw new InvalidSchemaMappingError()
|
||||
|
Loading…
Reference in New Issue
Block a user