Implement basic login & registration forms
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
5940b6e2b3
commit
a1d04d652e
@ -47,9 +47,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prebuild": "pnpm run lint && rimraf lib",
|
||||
"build": "tsc",
|
||||
"postbuild": "fse copy --all --dereference --preserveTimestamps --keepExisting=false --quiet --errorOnExist=false src/resources lib/resources",
|
||||
"build": "pnpm run lint && rimraf lib && tsc && fse copy --all --dereference --preserveTimestamps --keepExisting=false --quiet --errorOnExist=false src/resources lib/resources",
|
||||
"app": "tsc && node lib/index.js",
|
||||
"prepare": "pnpm run build",
|
||||
"docs:build": "typedoc --options typedoc.json",
|
||||
|
5
src/auth/AuthenticatableAlreadyExistsError.ts
Normal file
5
src/auth/AuthenticatableAlreadyExistsError.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {ErrorWithContext} from '../util'
|
||||
|
||||
export class AuthenticatableAlreadyExistsError extends ErrorWithContext {
|
||||
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import {Inject, Injectable} from '../di'
|
||||
import {EventBus} from '../event/EventBus'
|
||||
import {Awaitable, Maybe} from '../util'
|
||||
import {Authenticatable, AuthenticatableRepository} from './types'
|
||||
import {Authenticatable, AuthenticatableCredentials, AuthenticatableRepository} from './types'
|
||||
import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent'
|
||||
import {UserFlushedEvent} from './event/UserFlushedEvent'
|
||||
import {UserAuthenticationResumedEvent} from './event/UserAuthenticationResumedEvent'
|
||||
import {Logging} from '../service/Logging'
|
||||
|
||||
/**
|
||||
* Base-class for a context that authenticates users and manages security.
|
||||
@ -14,6 +15,9 @@ export abstract class SecurityContext {
|
||||
@Inject()
|
||||
protected readonly bus!: EventBus
|
||||
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
/** The currently authenticated user, if one exists. */
|
||||
private authenticatedUser?: Authenticatable
|
||||
|
||||
@ -57,7 +61,7 @@ export abstract class SecurityContext {
|
||||
* unauthenticated implicitly.
|
||||
* @param credentials
|
||||
*/
|
||||
async attemptOnce(credentials: Record<string, string>): Promise<Maybe<Authenticatable>> {
|
||||
async attemptOnce(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
||||
const user = await this.repository.getByCredentials(credentials)
|
||||
if ( user ) {
|
||||
await this.authenticateOnce(user)
|
||||
@ -71,7 +75,7 @@ export abstract class SecurityContext {
|
||||
* authentication will be persisted.
|
||||
* @param credentials
|
||||
*/
|
||||
async attempt(credentials: Record<string, string>): Promise<Maybe<Authenticatable>> {
|
||||
async attempt(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
||||
const user = await this.repository.getByCredentials(credentials)
|
||||
if ( user ) {
|
||||
await this.authenticate(user)
|
||||
@ -108,6 +112,8 @@ export abstract class SecurityContext {
|
||||
*/
|
||||
async resume(): Promise<void> {
|
||||
const credentials = await this.getCredentials()
|
||||
this.logging.debug('resume:')
|
||||
this.logging.debug(credentials)
|
||||
const user = await this.repository.getByCredentials(credentials)
|
||||
if ( user ) {
|
||||
this.authenticatedUser = user
|
||||
@ -125,7 +131,7 @@ export abstract class SecurityContext {
|
||||
* Get the credentials for the current user from whatever storage medium
|
||||
* the context's host provides.
|
||||
*/
|
||||
abstract getCredentials(): Awaitable<Record<string, string>>
|
||||
abstract getCredentials(): Awaitable<AuthenticatableCredentials>
|
||||
|
||||
/**
|
||||
* Get the currently authenticated user, if one exists.
|
||||
@ -138,6 +144,8 @@ export abstract class SecurityContext {
|
||||
* Returns true if there is a currently authenticated user.
|
||||
*/
|
||||
hasUser(): boolean {
|
||||
this.logging.debug('hasUser?')
|
||||
this.logging.debug(this.authenticatedUser)
|
||||
return Boolean(this.authenticatedUser)
|
||||
}
|
||||
}
|
||||
|
@ -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 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,
|
||||
})
|
||||
}
|
||||
|
||||
public getLogin(): ResponseFactory {
|
||||
return view('@extollo:auth:login')
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,17 @@
|
||||
import {FormRequest, ValidationRules} from '../../forms'
|
||||
import {Is, Str} from '../../forms/rules/rules'
|
||||
import {Singleton} from '../../di'
|
||||
|
||||
export interface BasicLoginCredentials {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
import {AuthenticatableCredentials} from '../types'
|
||||
|
||||
@Singleton()
|
||||
export class BasicLoginFormRequest extends FormRequest<BasicLoginCredentials> {
|
||||
export class BasicLoginFormRequest extends FormRequest<AuthenticatableCredentials> {
|
||||
protected getRules(): ValidationRules {
|
||||
return {
|
||||
username: [
|
||||
identifier: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
password: [
|
||||
credential: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
|
22
src/auth/basic-ui/BasicRegisterFormRequest.ts
Normal file
22
src/auth/basic-ui/BasicRegisterFormRequest.ts
Normal file
@ -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,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import {SecurityContext} from '../SecurityContext'
|
||||
import {Inject, Injectable} from '../../di'
|
||||
import {Session} from '../../http/session/Session'
|
||||
import {Awaitable} from '../../util'
|
||||
import {AuthenticatableRepository} from '../types'
|
||||
import {AuthenticatableCredentials, AuthenticatableRepository} from '../types'
|
||||
|
||||
/**
|
||||
* Security context implementation that uses the session as storage.
|
||||
@ -19,9 +19,10 @@ export class SessionSecurityContext extends SecurityContext {
|
||||
super(repository, 'session')
|
||||
}
|
||||
|
||||
getCredentials(): Awaitable<Record<string, string>> {
|
||||
getCredentials(): Awaitable<AuthenticatableCredentials> {
|
||||
return {
|
||||
securityIdentifier: this.session.get('extollo.auth.securityIdentifier'),
|
||||
identifier: '',
|
||||
credential: this.session.get('extollo.auth.securityIdentifier'),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,11 +24,11 @@ export class ORMUser extends Model<ORMUser> implements Authenticatable {
|
||||
|
||||
/** The user's first name. */
|
||||
@Field(FieldType.varchar, 'first_name')
|
||||
public firstName!: string
|
||||
public firstName?: string
|
||||
|
||||
/** The user's last name. */
|
||||
@Field(FieldType.varchar, 'last_name')
|
||||
public lastName!: string
|
||||
public lastName?: string
|
||||
|
||||
/** The hashed and salted password of the user. */
|
||||
@Field(FieldType.varchar, 'password_hash')
|
||||
|
@ -1,13 +1,22 @@
|
||||
import {Authenticatable, AuthenticatableIdentifier, AuthenticatableRepository} from '../types'
|
||||
import {
|
||||
Authenticatable,
|
||||
AuthenticatableCredentials,
|
||||
AuthenticatableIdentifier,
|
||||
AuthenticatableRepository,
|
||||
} from '../types'
|
||||
import {Awaitable, Maybe} from '../../util'
|
||||
import {ORMUser} from './ORMUser'
|
||||
import {Injectable} from '../../di'
|
||||
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>()
|
||||
@ -21,21 +30,36 @@ export class ORMUserRepository extends AuthenticatableRepository {
|
||||
* If username/password are specified, look up the user and verify the password.
|
||||
* @param credentials
|
||||
*/
|
||||
async getByCredentials(credentials: Record<string, string>): Promise<Maybe<Authenticatable>> {
|
||||
if ( credentials.securityIdentifier ) {
|
||||
async getByCredentials(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
||||
if ( !credentials.identifier && credentials.credential ) {
|
||||
return ORMUser.query<ORMUser>()
|
||||
.where('username', '=', credentials.securityIdentifier)
|
||||
.where('username', '=', credentials.credential)
|
||||
.first()
|
||||
}
|
||||
|
||||
if ( credentials.username && credentials.password ) {
|
||||
if ( credentials.identifier && credentials.credential ) {
|
||||
const user = await ORMUser.query<ORMUser>()
|
||||
.where('username', '=', credentials.username)
|
||||
.where('username', '=', credentials.identifier)
|
||||
.first()
|
||||
|
||||
if ( user && await user.verifyPassword(credentials.password) ) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,11 @@ import {Awaitable, JSONState, Maybe, Rehydratable} from '../util'
|
||||
/** Value that can be used to uniquely identify a user. */
|
||||
export type AuthenticatableIdentifier = string | number
|
||||
|
||||
export interface AuthenticatableCredentials {
|
||||
identifier: string,
|
||||
credential: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for entities that can be authenticated.
|
||||
*/
|
||||
@ -32,5 +37,7 @@ export abstract class AuthenticatableRepository {
|
||||
* Returns the user if the credentials are valid.
|
||||
* @param credentials
|
||||
*/
|
||||
abstract getByCredentials(credentials: Record<string, string>): Awaitable<Maybe<Authenticatable>>
|
||||
abstract getByCredentials(credentials: AuthenticatableCredentials): Awaitable<Maybe<Authenticatable>>
|
||||
|
||||
abstract createByCredentials(credentials: AuthenticatableCredentials): Awaitable<Authenticatable>
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
import {ValidationResult, ValidatorFunction, ValidatorFunctionParams} from './types'
|
||||
import {isJSON} from '../../util'
|
||||
|
||||
/**
|
||||
@ -221,6 +221,24 @@ function lengthMax(len: number): ValidatorFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator function that requires the input value to match a `${field}Confirm` field's value.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
* @param params
|
||||
*/
|
||||
function confirmed(fieldName: string, inputValue: unknown, params: ValidatorFunctionParams): ValidationResult {
|
||||
const confirmedFieldName = `${fieldName}Confirm`
|
||||
if ( inputValue === params.data[confirmedFieldName] ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `confirmation does not match`,
|
||||
}
|
||||
}
|
||||
|
||||
export const Str = {
|
||||
alpha,
|
||||
alphaNum,
|
||||
@ -242,4 +260,5 @@ export const Str = {
|
||||
length,
|
||||
lengthMin,
|
||||
lengthMax,
|
||||
confirmed,
|
||||
}
|
||||
|
@ -18,11 +18,11 @@ export default class CreateUsersTableMigration extends Migration {
|
||||
|
||||
table.column('first_name')
|
||||
.type(FieldType.varchar)
|
||||
.required()
|
||||
.nullable()
|
||||
|
||||
table.column('last_name')
|
||||
.type(FieldType.varchar)
|
||||
.required()
|
||||
.nullable()
|
||||
|
||||
table.column('password_hash')
|
||||
.type(FieldType.text)
|
||||
|
@ -612,6 +612,8 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
}
|
||||
|
||||
const row = this.buildInsertFieldObject()
|
||||
this.logging.debug('Insert field object:')
|
||||
this.logging.debug(row)
|
||||
const returnable = new Collection<string>([this.keyName(), ...Object.keys(row)])
|
||||
|
||||
const result = await this.query()
|
||||
@ -808,10 +810,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
private buildInsertFieldObject(): EscapeValueObject {
|
||||
const ctor = this.constructor as typeof Model
|
||||
|
||||
this.logging.debug(`buildInsertFieldObject populateKeyOnInsert? ${ctor.populateKeyOnInsert}; keyName: ${this.keyName()}`)
|
||||
|
||||
return getFieldsMeta(this)
|
||||
.pipe()
|
||||
.unless(ctor.populateKeyOnInsert, fields => {
|
||||
return fields.where('modelKey', '!=', this.keyName())
|
||||
return fields.where('databaseKey', '!=', this.keyName())
|
||||
})
|
||||
.get()
|
||||
.keyMap('databaseKey', inst => (this as any)[inst.modelKey])
|
||||
|
@ -164,12 +164,8 @@ export class PostgresSchema extends Schema {
|
||||
.pluck<string>('column_name')
|
||||
.each(col => idx.field(col))
|
||||
})
|
||||
.when(groupedIndexes[key]?.[0]?.indisprimary, idx => {
|
||||
idx.primary()
|
||||
})
|
||||
.when(groupedIndexes[key]?.[0]?.indisunique, idx => {
|
||||
idx.unique()
|
||||
})
|
||||
.when(groupedIndexes[key]?.[0]?.indisprimary, idx => idx.primary())
|
||||
.when(groupedIndexes[key]?.[0]?.indisunique, idx => idx.unique())
|
||||
.get()
|
||||
.flagAsExistingInSchema()
|
||||
}
|
||||
|
@ -6,9 +6,11 @@ import {FieldType} from '../types'
|
||||
* Model used to fetch & store sessions from the ORMSession driver.
|
||||
*/
|
||||
export class SessionModel extends Model<SessionModel> {
|
||||
protected static table = 'sessions'; // FIXME allow configuring
|
||||
protected static table = 'sessions' // FIXME allow configuring
|
||||
|
||||
protected static key = 'session_uuid';
|
||||
protected static key = 'session_uuid'
|
||||
|
||||
protected static populateKeyOnInsert = true
|
||||
|
||||
@Field(FieldType.varchar, 'session_uuid')
|
||||
public uuid!: string;
|
||||
|
@ -8,5 +8,9 @@ block content
|
||||
each error in errors
|
||||
p.form-error-message #{error}
|
||||
|
||||
form(method='post' enctype='multipart/form-data')
|
||||
block form
|
||||
if formAction
|
||||
form(method='post' enctype='multipart/form-data' action=formAction)
|
||||
block form
|
||||
else
|
||||
form(method='post' enctype='multipart/form-data')
|
||||
block form
|
||||
|
@ -8,11 +8,11 @@ block heading
|
||||
|
||||
block form
|
||||
.form-label-group
|
||||
input#inputUsername.form-control(type='text' name='username' value=(formData ? formData.username : '') required placeholder='Username' autofocus)
|
||||
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='password' required placeholder='Password')
|
||||
input#inputPassword.form-control(type='password' name='credential' required placeholder='Password')
|
||||
label(for='inputPassword') Password
|
||||
|
||||
|
||||
@ -21,4 +21,4 @@ block form
|
||||
|
||||
.text-center
|
||||
span.small Need an account?
|
||||
a(href='./register') Register here.
|
||||
a(href=named('@auth.register')) Register here.
|
||||
|
26
src/resources/views/auth/register.pug
Normal file
26
src/resources/views/auth/register.pug
Normal file
@ -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.
|
@ -82,7 +82,8 @@ export class HTTPServer extends Unit {
|
||||
}
|
||||
|
||||
public get handler(): RequestListener {
|
||||
const timeout = this.config.get('server.timeout', 10000)
|
||||
// const timeout = this.config.get('server.timeout', 10000)
|
||||
const timeout = 0 // temporarily disable this because it is causing problems
|
||||
|
||||
return async (request: IncomingMessage, response: ServerResponse) => {
|
||||
const extolloReq = new Request(request, response)
|
||||
|
@ -8,7 +8,7 @@ export type PipeOperator<T, T2> = (subject: T) => T2
|
||||
/**
|
||||
* A closure that maps a given pipe item to an item of the same type.
|
||||
*/
|
||||
export type ReflexivePipeOperator<T> = (subject: T) => T|void
|
||||
export type ReflexivePipeOperator<T> = (subject: T) => T
|
||||
|
||||
/**
|
||||
* A condition or condition-resolving function for pipe methods.
|
||||
@ -97,7 +97,7 @@ export class Pipe<T> {
|
||||
*/
|
||||
when(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
|
||||
if ( (typeof check === 'function' && check(this.subject)) || check ) {
|
||||
Pipe.wrap(op(this.subject))
|
||||
return Pipe.wrap(op(this.subject))
|
||||
}
|
||||
|
||||
return this
|
||||
@ -115,8 +115,7 @@ export class Pipe<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
Pipe.wrap(op(this.subject))
|
||||
return this
|
||||
return Pipe.wrap(op(this.subject))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,12 +52,14 @@ export function withTimeout<T>(timeout: number, promise: Promise<T>): TimeoutSub
|
||||
run: async () => {
|
||||
let expired = false
|
||||
let resolved = false
|
||||
setTimeout(() => {
|
||||
expired = true
|
||||
if ( !resolved ) {
|
||||
timeoutHandler()
|
||||
}
|
||||
}, timeout)
|
||||
if ( timeout ) {
|
||||
setTimeout(() => {
|
||||
expired = true
|
||||
if ( !resolved ) {
|
||||
timeoutHandler()
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
const result: T = await promise
|
||||
resolved = true
|
||||
|
@ -41,7 +41,7 @@ export class PugViewEngine extends ViewEngine {
|
||||
return {
|
||||
basedir: templateName ? this.resolveBasePath(templateName).toLocal : this.path.toLocal,
|
||||
debug: this.debug,
|
||||
compileDebug: this.debug,
|
||||
// compileDebug: this.debug,
|
||||
globals: [],
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user