From a1d04d652eb71023de3984c67d2bfab39c656221 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 21 Sep 2021 22:25:51 -0500 Subject: [PATCH] Implement basic login & registration forms --- package.json | 4 +- src/auth/AuthenticatableAlreadyExistsError.ts | 5 + src/auth/SecurityContext.ts | 16 ++- src/auth/basic-ui/BasicLoginController.ts | 128 +++++++++++++++++- src/auth/basic-ui/BasicLoginFormRequest.ts | 12 +- src/auth/basic-ui/BasicRegisterFormRequest.ts | 22 +++ src/auth/contexts/SessionSecurityContext.ts | 7 +- src/auth/orm/ORMUser.ts | 4 +- src/auth/orm/ORMUserRepository.ts | 40 ++++-- src/auth/types.ts | 9 +- src/forms/rules/strings.ts | 21 ++- ...0:31:00.000Z_CreateUsersTable.migration.ts | 4 +- src/orm/model/Model.ts | 6 +- src/orm/schema/PostgresSchema.ts | 8 +- src/orm/support/SessionModel.ts | 6 +- src/resources/views/auth/form.pug | 8 +- src/resources/views/auth/login.pug | 6 +- src/resources/views/auth/register.pug | 26 ++++ src/service/HTTPServer.ts | 3 +- src/util/support/Pipe.ts | 7 +- src/util/support/timeout.ts | 14 +- src/views/PugViewEngine.ts | 2 +- 22 files changed, 294 insertions(+), 64 deletions(-) create mode 100644 src/auth/AuthenticatableAlreadyExistsError.ts create mode 100644 src/auth/basic-ui/BasicRegisterFormRequest.ts create mode 100644 src/resources/views/auth/register.pug diff --git a/package.json b/package.json index 9809cba..0467288 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/auth/AuthenticatableAlreadyExistsError.ts b/src/auth/AuthenticatableAlreadyExistsError.ts new file mode 100644 index 0000000..f7b1a9a --- /dev/null +++ b/src/auth/AuthenticatableAlreadyExistsError.ts @@ -0,0 +1,5 @@ +import {ErrorWithContext} from '../util' + +export class AuthenticatableAlreadyExistsError extends ErrorWithContext { + +} diff --git a/src/auth/SecurityContext.ts b/src/auth/SecurityContext.ts index a04f39f..54c1482 100644 --- a/src/auth/SecurityContext.ts +++ b/src/auth/SecurityContext.ts @@ -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): Promise> { + async attemptOnce(credentials: AuthenticatableCredentials): Promise> { 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): Promise> { + async attempt(credentials: AuthenticatableCredentials): Promise> { const user = await this.repository.getByCredentials(credentials) if ( user ) { await this.authenticate(user) @@ -108,6 +112,8 @@ export abstract class SecurityContext { */ async resume(): Promise { 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> + abstract getCredentials(): Awaitable /** * 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) } } diff --git a/src/auth/basic-ui/BasicLoginController.ts b/src/auth/basic-ui/BasicLoginController.ts index f7a264c..6dcde06 100644 --- a/src/auth/basic-ui/BasicLoginController.ts +++ b/src/auth/basic-ui/BasicLoginController.ts @@ -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 = request.make(BasicLoginController) return controller.getLogin() }) + .pre('@auth:guest') .alias('@auth.login') Route.post('login', (request: Request) => { const controller = request.make(BasicLoginController) - return controller.getLogin() + return controller.attemptLogin() }) + .pre('@auth:guest') .alias('@auth.login.attempt') - }) + + Route.any('logout', (request: Request) => { + const controller = request.make(BasicLoginController) + return controller.attemptLogout() + }) + .pre('@auth:required') + .alias('@auth.logout') + + if ( enableRegistration ) { + Route.get('register', (request: Request) => { + const controller = request.make(BasicLoginController) + return controller.getRegistration() + }) + .pre('@auth:guest') + .alias('@auth.register') + + Route.post('register', (request: Request) => { + const controller = 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 { + const form = this.request.make(BasicLoginFormRequest) + + try { + const data: Valid = 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 { + await this.security.flush() + return this.getMessageView('You have been logged out.') + } + + public async attemptRegister(): Promise { + const form = this.request.make(BasicRegisterFormRequest) + + try { + const data: Valid = 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, + }) } } diff --git a/src/auth/basic-ui/BasicLoginFormRequest.ts b/src/auth/basic-ui/BasicLoginFormRequest.ts index b83d45a..b2d80d5 100644 --- a/src/auth/basic-ui/BasicLoginFormRequest.ts +++ b/src/auth/basic-ui/BasicLoginFormRequest.ts @@ -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 { +export class BasicLoginFormRequest extends FormRequest { protected getRules(): ValidationRules { return { - username: [ + identifier: [ Is.required, Str.lengthMin(1), ], - password: [ + credential: [ Is.required, Str.lengthMin(1), ], diff --git a/src/auth/basic-ui/BasicRegisterFormRequest.ts b/src/auth/basic-ui/BasicRegisterFormRequest.ts new file mode 100644 index 0000000..699f212 --- /dev/null +++ b/src/auth/basic-ui/BasicRegisterFormRequest.ts @@ -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 { + protected getRules(): ValidationRules { + return { + identifier: [ + Is.required, + Str.lengthMin(1), + Str.alphaNum, + ], + credential: [ + Is.required, + Str.lengthMin(8), + Str.confirmed, + ], + } + } +} diff --git a/src/auth/contexts/SessionSecurityContext.ts b/src/auth/contexts/SessionSecurityContext.ts index 3d51c99..83d859f 100644 --- a/src/auth/contexts/SessionSecurityContext.ts +++ b/src/auth/contexts/SessionSecurityContext.ts @@ -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> { + getCredentials(): Awaitable { return { - securityIdentifier: this.session.get('extollo.auth.securityIdentifier'), + identifier: '', + credential: this.session.get('extollo.auth.securityIdentifier'), } } diff --git a/src/auth/orm/ORMUser.ts b/src/auth/orm/ORMUser.ts index 95ebf78..3356450 100644 --- a/src/auth/orm/ORMUser.ts +++ b/src/auth/orm/ORMUser.ts @@ -24,11 +24,11 @@ export class ORMUser extends Model 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') diff --git a/src/auth/orm/ORMUserRepository.ts b/src/auth/orm/ORMUserRepository.ts index d6dae16..1ba409a 100644 --- a/src/auth/orm/ORMUserRepository.ts +++ b/src/auth/orm/ORMUserRepository.ts @@ -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> { return ORMUser.query() @@ -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): Promise> { - if ( credentials.securityIdentifier ) { + async getByCredentials(credentials: AuthenticatableCredentials): Promise> { + if ( !credentials.identifier && credentials.credential ) { return ORMUser.query() - .where('username', '=', credentials.securityIdentifier) + .where('username', '=', credentials.credential) .first() } - if ( credentials.username && credentials.password ) { + if ( credentials.identifier && credentials.credential ) { const user = await ORMUser.query() - .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 { + if ( await this.getByCredentials(credentials) ) { + throw new AuthenticatableAlreadyExistsError(`Authenticatable already exists with credentials.`, { + identifier: credentials.identifier, + }) + } + + const user = this.injector.make(ORMUser) + user.username = credentials.identifier + await user.setPassword(credentials.credential) + await user.save() + + return user + } } diff --git a/src/auth/types.ts b/src/auth/types.ts index e575c9f..8fe29b3 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -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): Awaitable> + abstract getByCredentials(credentials: AuthenticatableCredentials): Awaitable> + + abstract createByCredentials(credentials: AuthenticatableCredentials): Awaitable } diff --git a/src/forms/rules/strings.ts b/src/forms/rules/strings.ts index abe480c..b6d3cd7 100644 --- a/src/forms/rules/strings.ts +++ b/src/forms/rules/strings.ts @@ -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, } diff --git a/src/migrations/2021-07-24T10:31:00.000Z_CreateUsersTable.migration.ts b/src/migrations/2021-07-24T10:31:00.000Z_CreateUsersTable.migration.ts index c156a36..1070172 100644 --- a/src/migrations/2021-07-24T10:31:00.000Z_CreateUsersTable.migration.ts +++ b/src/migrations/2021-07-24T10:31:00.000Z_CreateUsersTable.migration.ts @@ -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) diff --git a/src/orm/model/Model.ts b/src/orm/model/Model.ts index 06d517b..a05003a 100644 --- a/src/orm/model/Model.ts +++ b/src/orm/model/Model.ts @@ -612,6 +612,8 @@ export abstract class Model> extends AppClass implements Bus } const row = this.buildInsertFieldObject() + this.logging.debug('Insert field object:') + this.logging.debug(row) const returnable = new Collection([this.keyName(), ...Object.keys(row)]) const result = await this.query() @@ -808,10 +810,12 @@ export abstract class Model> 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]) diff --git a/src/orm/schema/PostgresSchema.ts b/src/orm/schema/PostgresSchema.ts index 51efb37..c8d3a95 100644 --- a/src/orm/schema/PostgresSchema.ts +++ b/src/orm/schema/PostgresSchema.ts @@ -164,12 +164,8 @@ export class PostgresSchema extends Schema { .pluck('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() } diff --git a/src/orm/support/SessionModel.ts b/src/orm/support/SessionModel.ts index 3f5bc72..d9e78cd 100644 --- a/src/orm/support/SessionModel.ts +++ b/src/orm/support/SessionModel.ts @@ -6,9 +6,11 @@ import {FieldType} from '../types' * Model used to fetch & store sessions from the ORMSession driver. */ export class SessionModel extends Model { - 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; diff --git a/src/resources/views/auth/form.pug b/src/resources/views/auth/form.pug index 242c0db..f6431da 100644 --- a/src/resources/views/auth/form.pug +++ b/src/resources/views/auth/form.pug @@ -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 diff --git a/src/resources/views/auth/login.pug b/src/resources/views/auth/login.pug index 3738d9a..0c340c1 100644 --- a/src/resources/views/auth/login.pug +++ b/src/resources/views/auth/login.pug @@ -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. diff --git a/src/resources/views/auth/register.pug b/src/resources/views/auth/register.pug new file mode 100644 index 0000000..4e4b32c --- /dev/null +++ b/src/resources/views/auth/register.pug @@ -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. diff --git a/src/service/HTTPServer.ts b/src/service/HTTPServer.ts index 24af0ac..e92a40b 100644 --- a/src/service/HTTPServer.ts +++ b/src/service/HTTPServer.ts @@ -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) diff --git a/src/util/support/Pipe.ts b/src/util/support/Pipe.ts index 6700071..599994e 100644 --- a/src/util/support/Pipe.ts +++ b/src/util/support/Pipe.ts @@ -8,7 +8,7 @@ export type PipeOperator = (subject: T) => T2 /** * A closure that maps a given pipe item to an item of the same type. */ -export type ReflexivePipeOperator = (subject: T) => T|void +export type ReflexivePipeOperator = (subject: T) => T /** * A condition or condition-resolving function for pipe methods. @@ -97,7 +97,7 @@ export class Pipe { */ when(check: PipeCondition, op: ReflexivePipeOperator): Pipe { 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 { return this } - Pipe.wrap(op(this.subject)) - return this + return Pipe.wrap(op(this.subject)) } /** diff --git a/src/util/support/timeout.ts b/src/util/support/timeout.ts index 25c053d..0aa1e71 100644 --- a/src/util/support/timeout.ts +++ b/src/util/support/timeout.ts @@ -52,12 +52,14 @@ export function withTimeout(timeout: number, promise: Promise): 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 diff --git a/src/views/PugViewEngine.ts b/src/views/PugViewEngine.ts index 69a7d11..80b0c25 100644 --- a/src/views/PugViewEngine.ts +++ b/src/views/PugViewEngine.ts @@ -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: [], } }