22 changed files with 294 additions and 64 deletions
@ -0,0 +1,5 @@ |
|||
import {ErrorWithContext} from '../util' |
|||
|
|||
export class AuthenticatableAlreadyExistsError extends ErrorWithContext { |
|||
|
|||
} |
@ -1,29 +1,145 @@ |
|||
import {Controller} from '../../http/Controller' |
|||
import {Injectable} from '../../di' |
|||
import {Route} from '../../http/routing/Route' |
|||
import {Inject, Injectable} from '../../di' |
|||
import {ResponseObject, Route} from '../../http/routing/Route' |
|||
import {Request} from '../../http/lifecycle/Request' |
|||
import {view} from '../../http/response/ViewResponseFactory' |
|||
import {ResponseFactory} from '../../http/response/ResponseFactory' |
|||
import {SecurityContext} from '../SecurityContext' |
|||
import {BasicLoginFormRequest} from './BasicLoginFormRequest' |
|||
import {Routing} from '../../service/Routing' |
|||
import {Valid, ValidationError} from '../../forms' |
|||
import {AuthenticatableCredentials} from '../types' |
|||
import {BasicRegisterFormRequest} from './BasicRegisterFormRequest' |
|||
import {AuthenticatableAlreadyExistsError} from '../AuthenticatableAlreadyExistsError' |
|||
import {Session} from '../../http/session/Session' |
|||
import {temporary} from '../../http/response/TemporaryRedirectResponseFactory' |
|||
|
|||
@Injectable() |
|||
export class BasicLoginController extends Controller { |
|||
public static routes(): void { |
|||
public static routes({ enableRegistration = true } = {}): void { |
|||
Route.group('auth', () => { |
|||
Route.get('login', (request: Request) => { |
|||
const controller = <BasicLoginController> request.make(BasicLoginController) |
|||
return controller.getLogin() |
|||
}) |
|||
.pre('@auth:guest') |
|||
.alias('@auth.login') |
|||
|
|||
Route.post('login', (request: Request) => { |
|||
const controller = <BasicLoginController> request.make(BasicLoginController) |
|||
return controller.getLogin() |
|||
return controller.attemptLogin() |
|||
}) |
|||
.pre('@auth:guest') |
|||
.alias('@auth.login.attempt') |
|||
}) |
|||
|
|||
Route.any('logout', (request: Request) => { |
|||
const controller = <BasicLoginController> request.make(BasicLoginController) |
|||
return controller.attemptLogout() |
|||
}) |
|||
.pre('@auth:required') |
|||
.alias('@auth.logout') |
|||
|
|||
if ( enableRegistration ) { |
|||
Route.get('register', (request: Request) => { |
|||
const controller = <BasicLoginController> request.make(BasicLoginController) |
|||
return controller.getRegistration() |
|||
}) |
|||
.pre('@auth:guest') |
|||
.alias('@auth.register') |
|||
|
|||
Route.post('register', (request: Request) => { |
|||
const controller = <BasicLoginController> request.make(BasicLoginController) |
|||
return controller.attemptRegister() |
|||
}) |
|||
.pre('@auth:guest') |
|||
.alias('@auth.register.attempt') |
|||
} |
|||
}).pre('@auth:web') |
|||
} |
|||
|
|||
@Inject() |
|||
protected readonly security!: SecurityContext |
|||
|
|||
@Inject() |
|||
protected readonly routing!: Routing |
|||
|
|||
@Inject() |
|||
protected readonly session!: Session |
|||
|
|||
public getLogin(): ResponseFactory { |
|||
return view('@extollo:auth:login') |
|||
return this.getLoginView() |
|||
} |
|||
|
|||
public getRegistration(): ResponseFactory { |
|||
return this.getRegistrationView() |
|||
} |
|||
|
|||
public async attemptLogin(): Promise<ResponseObject> { |
|||
const form = <BasicLoginFormRequest> this.request.make(BasicLoginFormRequest) |
|||
|
|||
try { |
|||
const data: Valid<AuthenticatableCredentials> = await form.get() |
|||
const user = await this.security.attempt(data) |
|||
if ( user ) { |
|||
const intention = this.session.get('auth.intention', '/') |
|||
this.session.forget('auth.intention') |
|||
return temporary(intention) |
|||
} |
|||
|
|||
return this.getLoginView(['Invalid username/password.']) |
|||
} catch (e: unknown) { |
|||
if ( e instanceof ValidationError ) { |
|||
return this.getLoginView(e.errors.all()) |
|||
} |
|||
|
|||
throw e |
|||
} |
|||
} |
|||
|
|||
public async attemptLogout(): Promise<ResponseObject> { |
|||
await this.security.flush() |
|||
return this.getMessageView('You have been logged out.') |
|||
} |
|||
|
|||
public async attemptRegister(): Promise<ResponseObject> { |
|||
const form = <BasicRegisterFormRequest> this.request.make(BasicRegisterFormRequest) |
|||
|
|||
try { |
|||
const data: Valid<AuthenticatableCredentials> = await form.get() |
|||
const user = await this.security.repository.createByCredentials(data) |
|||
await this.security.authenticate(user) |
|||
|
|||
const intention = this.session.get('auth.intention', '/') |
|||
this.session.forget('auth.intention') |
|||
return temporary(intention) |
|||
} catch (e: unknown) { |
|||
if ( e instanceof ValidationError ) { |
|||
return this.getRegistrationView(e.errors.all()) |
|||
} else if ( e instanceof AuthenticatableAlreadyExistsError ) { |
|||
return this.getRegistrationView(['A user with that username already exists.']) |
|||
} |
|||
|
|||
throw e |
|||
} |
|||
} |
|||
|
|||
protected getLoginView(errors?: string[]): ResponseFactory { |
|||
return view('@extollo:auth:login', { |
|||
formAction: this.routing.getNamedPath('@auth.login.attempt').toRemote, |
|||
errors, |
|||
}) |
|||
} |
|||
|
|||
protected getRegistrationView(errors?: string[]): ResponseFactory { |
|||
return view('@extollo:auth:register', { |
|||
formAction: this.routing.getNamedPath('@auth.register.attempt').toRemote, |
|||
errors, |
|||
}) |
|||
} |
|||
|
|||
protected getMessageView(message: string): ResponseFactory { |
|||
return view('@extollo:auth:message', { |
|||
message, |
|||
}) |
|||
} |
|||
} |
|||
|
@ -0,0 +1,22 @@ |
|||
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, |
|||
], |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
extends ./form |
|||
|
|||
block head |
|||
title Register | #{config('app.name', 'Extollo')} |
|||
|
|||
block heading |
|||
| Register to continue |
|||
|
|||
block form |
|||
.form-label-group |
|||
input#inputUsername.form-control(type='text' name='identifier' value=(formData ? formData.username : '') required placeholder='Username' autofocus) |
|||
label(for='inputUsername') Username |
|||
|
|||
.form-label-group |
|||
input#inputPassword.form-control(type='password' name='credential' required placeholder='Password') |
|||
label(for='inputPassword') Password |
|||
|
|||
.form-label-group |
|||
input#inputPasswordConfirm.form-control(type='password' name='credentialConfirm' required placeholder='Confirm Password') |
|||
label(for='inputPassword') Confirm 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 |
|||
span.small Have an account? |
|||
a(href=named('@auth.login')) Login here. |
Loading…
Reference in new issue