Implement basic login & registration forms
continuous-integration/drone/push Build is failing Details

orm-types
Garrett Mills 3 years ago
parent 5940b6e2b3
commit a1d04d652e

@ -47,9 +47,7 @@
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"prebuild": "pnpm run lint && rimraf lib", "build": "pnpm run lint && rimraf lib && tsc && fse copy --all --dereference --preserveTimestamps --keepExisting=false --quiet --errorOnExist=false src/resources lib/resources",
"build": "tsc",
"postbuild": "fse copy --all --dereference --preserveTimestamps --keepExisting=false --quiet --errorOnExist=false src/resources lib/resources",
"app": "tsc && node lib/index.js", "app": "tsc && node lib/index.js",
"prepare": "pnpm run build", "prepare": "pnpm run build",
"docs:build": "typedoc --options typedoc.json", "docs:build": "typedoc --options typedoc.json",

@ -0,0 +1,5 @@
import {ErrorWithContext} from '../util'
export class AuthenticatableAlreadyExistsError extends ErrorWithContext {
}

@ -1,10 +1,11 @@
import {Inject, Injectable} from '../di' import {Inject, Injectable} from '../di'
import {EventBus} from '../event/EventBus' import {EventBus} from '../event/EventBus'
import {Awaitable, Maybe} from '../util' import {Awaitable, Maybe} from '../util'
import {Authenticatable, AuthenticatableRepository} from './types' import {Authenticatable, AuthenticatableCredentials, AuthenticatableRepository} from './types'
import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent' import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent'
import {UserFlushedEvent} from './event/UserFlushedEvent' import {UserFlushedEvent} from './event/UserFlushedEvent'
import {UserAuthenticationResumedEvent} from './event/UserAuthenticationResumedEvent' import {UserAuthenticationResumedEvent} from './event/UserAuthenticationResumedEvent'
import {Logging} from '../service/Logging'
/** /**
* Base-class for a context that authenticates users and manages security. * Base-class for a context that authenticates users and manages security.
@ -14,6 +15,9 @@ export abstract class SecurityContext {
@Inject() @Inject()
protected readonly bus!: EventBus protected readonly bus!: EventBus
@Inject()
protected readonly logging!: Logging
/** The currently authenticated user, if one exists. */ /** The currently authenticated user, if one exists. */
private authenticatedUser?: Authenticatable private authenticatedUser?: Authenticatable
@ -57,7 +61,7 @@ export abstract class SecurityContext {
* unauthenticated implicitly. * unauthenticated implicitly.
* @param credentials * @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) const user = await this.repository.getByCredentials(credentials)
if ( user ) { if ( user ) {
await this.authenticateOnce(user) await this.authenticateOnce(user)
@ -71,7 +75,7 @@ export abstract class SecurityContext {
* authentication will be persisted. * authentication will be persisted.
* @param credentials * @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) const user = await this.repository.getByCredentials(credentials)
if ( user ) { if ( user ) {
await this.authenticate(user) await this.authenticate(user)
@ -108,6 +112,8 @@ export abstract class SecurityContext {
*/ */
async resume(): Promise<void> { async resume(): Promise<void> {
const credentials = await this.getCredentials() const credentials = await this.getCredentials()
this.logging.debug('resume:')
this.logging.debug(credentials)
const user = await this.repository.getByCredentials(credentials) const user = await this.repository.getByCredentials(credentials)
if ( user ) { if ( user ) {
this.authenticatedUser = user this.authenticatedUser = user
@ -125,7 +131,7 @@ export abstract class SecurityContext {
* Get the credentials for the current user from whatever storage medium * Get the credentials for the current user from whatever storage medium
* the context's host provides. * the context's host provides.
*/ */
abstract getCredentials(): Awaitable<Record<string, string>> abstract getCredentials(): Awaitable<AuthenticatableCredentials>
/** /**
* Get the currently authenticated user, if one exists. * 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. * Returns true if there is a currently authenticated user.
*/ */
hasUser(): boolean { hasUser(): boolean {
this.logging.debug('hasUser?')
this.logging.debug(this.authenticatedUser)
return Boolean(this.authenticatedUser) return Boolean(this.authenticatedUser)
} }
} }

@ -1,29 +1,145 @@
import {Controller} from '../../http/Controller' import {Controller} from '../../http/Controller'
import {Injectable} from '../../di' import {Inject, Injectable} from '../../di'
import {Route} from '../../http/routing/Route' import {ResponseObject, Route} from '../../http/routing/Route'
import {Request} from '../../http/lifecycle/Request' import {Request} from '../../http/lifecycle/Request'
import {view} from '../../http/response/ViewResponseFactory' import {view} from '../../http/response/ViewResponseFactory'
import {ResponseFactory} from '../../http/response/ResponseFactory' 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() @Injectable()
export class BasicLoginController extends Controller { export class BasicLoginController extends Controller {
public static routes(): void { public static routes({ enableRegistration = true } = {}): void {
Route.group('auth', () => { Route.group('auth', () => {
Route.get('login', (request: Request) => { Route.get('login', (request: Request) => {
const controller = <BasicLoginController> request.make(BasicLoginController) const controller = <BasicLoginController> request.make(BasicLoginController)
return controller.getLogin() return controller.getLogin()
}) })
.pre('@auth:guest')
.alias('@auth.login') .alias('@auth.login')
Route.post('login', (request: Request) => { Route.post('login', (request: Request) => {
const controller = <BasicLoginController> request.make(BasicLoginController) const controller = <BasicLoginController> request.make(BasicLoginController)
return controller.getLogin() return controller.attemptLogin()
}) })
.pre('@auth:guest')
.alias('@auth.login.attempt') .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 { 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,
})
} }
} }

@ -1,21 +1,17 @@
import {FormRequest, ValidationRules} from '../../forms' import {FormRequest, ValidationRules} from '../../forms'
import {Is, Str} from '../../forms/rules/rules' import {Is, Str} from '../../forms/rules/rules'
import {Singleton} from '../../di' import {Singleton} from '../../di'
import {AuthenticatableCredentials} from '../types'
export interface BasicLoginCredentials {
username: string,
password: string,
}
@Singleton() @Singleton()
export class BasicLoginFormRequest extends FormRequest<BasicLoginCredentials> { export class BasicLoginFormRequest extends FormRequest<AuthenticatableCredentials> {
protected getRules(): ValidationRules { protected getRules(): ValidationRules {
return { return {
username: [ identifier: [
Is.required, Is.required,
Str.lengthMin(1), Str.lengthMin(1),
], ],
password: [ credential: [
Is.required, Is.required,
Str.lengthMin(1), Str.lengthMin(1),
], ],

@ -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 {Inject, Injectable} from '../../di'
import {Session} from '../../http/session/Session' import {Session} from '../../http/session/Session'
import {Awaitable} from '../../util' import {Awaitable} from '../../util'
import {AuthenticatableRepository} from '../types' import {AuthenticatableCredentials, AuthenticatableRepository} from '../types'
/** /**
* Security context implementation that uses the session as storage. * Security context implementation that uses the session as storage.
@ -19,9 +19,10 @@ export class SessionSecurityContext extends SecurityContext {
super(repository, 'session') super(repository, 'session')
} }
getCredentials(): Awaitable<Record<string, string>> { getCredentials(): Awaitable<AuthenticatableCredentials> {
return { 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. */ /** The user's first name. */
@Field(FieldType.varchar, 'first_name') @Field(FieldType.varchar, 'first_name')
public firstName!: string public firstName?: string
/** The user's last name. */ /** The user's last name. */
@Field(FieldType.varchar, 'last_name') @Field(FieldType.varchar, 'last_name')
public lastName!: string public lastName?: string
/** The hashed and salted password of the user. */ /** The hashed and salted password of the user. */
@Field(FieldType.varchar, 'password_hash') @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 {Awaitable, Maybe} from '../../util'
import {ORMUser} from './ORMUser' 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. * A user repository implementation that looks up users stored in the database.
*/ */
@Injectable() @Injectable()
export class ORMUserRepository extends AuthenticatableRepository { export class ORMUserRepository extends AuthenticatableRepository {
@Inject('injector')
protected readonly injector!: Container
/** Look up the user by their username. */ /** Look up the user by their username. */
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> { getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
return ORMUser.query<ORMUser>() 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. * If username/password are specified, look up the user and verify the password.
* @param credentials * @param credentials
*/ */
async getByCredentials(credentials: Record<string, string>): Promise<Maybe<Authenticatable>> { async getByCredentials(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
if ( credentials.securityIdentifier ) { if ( !credentials.identifier && credentials.credential ) {
return ORMUser.query<ORMUser>() return ORMUser.query<ORMUser>()
.where('username', '=', credentials.securityIdentifier) .where('username', '=', credentials.credential)
.first() .first()
} }
if ( credentials.username && credentials.password ) { if ( credentials.identifier && credentials.credential ) {
const user = await ORMUser.query<ORMUser>() const user = await ORMUser.query<ORMUser>()
.where('username', '=', credentials.username) .where('username', '=', credentials.identifier)
.first() .first()
if ( user && await user.verifyPassword(credentials.password) ) { if ( user && await user.verifyPassword(credentials.credential) ) {
return user 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. */ /** Value that can be used to uniquely identify a user. */
export type AuthenticatableIdentifier = string | number export type AuthenticatableIdentifier = string | number
export interface AuthenticatableCredentials {
identifier: string,
credential: string,
}
/** /**
* Base class for entities that can be authenticated. * Base class for entities that can be authenticated.
*/ */
@ -32,5 +37,7 @@ export abstract class AuthenticatableRepository {
* Returns the user if the credentials are valid. * Returns the user if the credentials are valid.
* @param credentials * @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' 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 = { export const Str = {
alpha, alpha,
alphaNum, alphaNum,
@ -242,4 +260,5 @@ export const Str = {
length, length,
lengthMin, lengthMin,
lengthMax, lengthMax,
confirmed,
} }

@ -18,11 +18,11 @@ export default class CreateUsersTableMigration extends Migration {
table.column('first_name') table.column('first_name')
.type(FieldType.varchar) .type(FieldType.varchar)
.required() .nullable()
table.column('last_name') table.column('last_name')
.type(FieldType.varchar) .type(FieldType.varchar)
.required() .nullable()
table.column('password_hash') table.column('password_hash')
.type(FieldType.text) .type(FieldType.text)

@ -612,6 +612,8 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
} }
const row = this.buildInsertFieldObject() 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 returnable = new Collection<string>([this.keyName(), ...Object.keys(row)])
const result = await this.query() const result = await this.query()
@ -808,10 +810,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
private buildInsertFieldObject(): EscapeValueObject { private buildInsertFieldObject(): EscapeValueObject {
const ctor = this.constructor as typeof Model const ctor = this.constructor as typeof Model
this.logging.debug(`buildInsertFieldObject populateKeyOnInsert? ${ctor.populateKeyOnInsert}; keyName: ${this.keyName()}`)
return getFieldsMeta(this) return getFieldsMeta(this)
.pipe() .pipe()
.unless(ctor.populateKeyOnInsert, fields => { .unless(ctor.populateKeyOnInsert, fields => {
return fields.where('modelKey', '!=', this.keyName()) return fields.where('databaseKey', '!=', this.keyName())
}) })
.get() .get()
.keyMap('databaseKey', inst => (this as any)[inst.modelKey]) .keyMap('databaseKey', inst => (this as any)[inst.modelKey])

@ -164,12 +164,8 @@ export class PostgresSchema extends Schema {
.pluck<string>('column_name') .pluck<string>('column_name')
.each(col => idx.field(col)) .each(col => idx.field(col))
}) })
.when(groupedIndexes[key]?.[0]?.indisprimary, idx => { .when(groupedIndexes[key]?.[0]?.indisprimary, idx => idx.primary())
idx.primary() .when(groupedIndexes[key]?.[0]?.indisunique, idx => idx.unique())
})
.when(groupedIndexes[key]?.[0]?.indisunique, idx => {
idx.unique()
})
.get() .get()
.flagAsExistingInSchema() .flagAsExistingInSchema()
} }

@ -6,9 +6,11 @@ import {FieldType} from '../types'
* Model used to fetch & store sessions from the ORMSession driver. * Model used to fetch & store sessions from the ORMSession driver.
*/ */
export class SessionModel extends Model<SessionModel> { 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') @Field(FieldType.varchar, 'session_uuid')
public uuid!: string; public uuid!: string;

@ -8,5 +8,9 @@ block content
each error in errors each error in errors
p.form-error-message #{error} p.form-error-message #{error}
form(method='post' enctype='multipart/form-data') if formAction
block form 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 block form
.form-label-group .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 label(for='inputUsername') Username
.form-label-group .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 label(for='inputPassword') Password
@ -21,4 +21,4 @@ block form
.text-center .text-center
span.small Need an account?&nbsp; span.small Need an account?&nbsp;
a(href='./register') Register here. a(href=named('@auth.register')) Register here.

@ -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?&nbsp;
a(href=named('@auth.login')) Login here.

@ -82,7 +82,8 @@ export class HTTPServer extends Unit {
} }
public get handler(): RequestListener { 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) => { return async (request: IncomingMessage, response: ServerResponse) => {
const extolloReq = new Request(request, response) 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. * 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. * 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> { when(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
if ( (typeof check === 'function' && check(this.subject)) || check ) { if ( (typeof check === 'function' && check(this.subject)) || check ) {
Pipe.wrap(op(this.subject)) return Pipe.wrap(op(this.subject))
} }
return this return this
@ -115,8 +115,7 @@ export class Pipe<T> {
return this return this
} }
Pipe.wrap(op(this.subject)) return Pipe.wrap(op(this.subject))
return this
} }
/** /**

@ -52,12 +52,14 @@ export function withTimeout<T>(timeout: number, promise: Promise<T>): TimeoutSub
run: async () => { run: async () => {
let expired = false let expired = false
let resolved = false let resolved = false
setTimeout(() => { if ( timeout ) {
expired = true setTimeout(() => {
if ( !resolved ) { expired = true
timeoutHandler() if ( !resolved ) {
} timeoutHandler()
}, timeout) }
}, timeout)
}
const result: T = await promise const result: T = await promise
resolved = true resolved = true

@ -41,7 +41,7 @@ export class PugViewEngine extends ViewEngine {
return { return {
basedir: templateName ? this.resolveBasePath(templateName).toLocal : this.path.toLocal, basedir: templateName ? this.resolveBasePath(templateName).toLocal : this.path.toLocal,
debug: this.debug, debug: this.debug,
compileDebug: this.debug, // compileDebug: this.debug,
globals: [], globals: [],
} }
} }

Loading…
Cancel
Save