This commit is contained in:
parent
cfd555723b
commit
506fb55c74
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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')
|
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
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user