Introduce async local storage for request access, more view globals, and improved welcome view
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Garrett Mills 2021-11-27 10:30:49 -06:00
parent b5eb407b55
commit 463076d182
8 changed files with 148 additions and 32 deletions

View File

@ -7,6 +7,8 @@ import {Middleware} from '../http/routing/Middleware'
import {AuthRequiredMiddleware} from './middleware/AuthRequiredMiddleware'
import {GuestRequiredMiddleware} from './middleware/GuestRequiredMiddleware'
import {SessionAuthMiddleware} from './middleware/SessionAuthMiddleware'
import {ViewEngine} from '../views/ViewEngine'
import {SecurityContext} from './context/SecurityContext'
@Injectable()
export class Authentication extends Unit {
@ -18,6 +20,13 @@ export class Authentication extends Unit {
async up(): Promise<void> {
this.middleware.registerNamespace('@auth', this.getMiddlewareResolver())
this.container().onResolve<ViewEngine>(ViewEngine)
.then((engine: ViewEngine) => {
engine.registerGlobalFactory('user', req => {
return () => req?.make<SecurityContext>(SecurityContext)?.getUser()
})
})
}
protected getMiddlewareResolver(): CanonicalResolver<StaticInstantiable<Middleware>> {

View File

@ -0,0 +1,32 @@
import { AsyncLocalStorage } from 'async_hooks'
import {Request} from './lifecycle/Request'
import {Singleton} from '../di'
import {ErrorWithContext} from '../util'
export class InvalidOutOfRequestAccessError extends ErrorWithContext {
constructor() {
super(`Attempted to access request via local storage outside of async lifecycle!`)
}
}
@Singleton()
export class RequestLocalStorage {
protected readonly store: AsyncLocalStorage<Request> = new AsyncLocalStorage<Request>()
get(): Request {
const req = this.store.getStore()
if ( !req ) {
throw new InvalidOutOfRequestAccessError()
}
return req
}
has(): boolean {
return Boolean(this.store.getStore())
}
run<T>(req: Request, closure: () => T): T {
return this.store.run(req, closure)
}
}

View File

@ -32,6 +32,7 @@ export * from './http/kernel/HTTPCookieJar'
export * from './http/lifecycle/Request'
export * from './http/lifecycle/Response'
export * from './http/RequestLocalStorage'
export * as api from './http/response/api'
export * from './http/response/DehydratedStateResponseFactory'

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 646.49 291.04"><defs><style>.cls-1{fill:#49686a;}.cls-2{fill:#f2c373;}.cls-3{fill:#2e5252;}.cls-4{fill:#ffe293;}</style></defs><polygon class="cls-1" points="26.46 242.4 136.57 3.29 226.81 204.64 189.06 204.64 131.33 87.19 66.31 242.4 26.46 242.4"/><polygon class="cls-2" points="225.72 209.03 207.89 246.79 170.13 246.79 187.96 209.03 225.72 209.03"/><polygon class="cls-3" points="165.75 244.59 183.57 206.84 131.33 98.16 111.54 145.41 165.75 244.59"/><polygon class="cls-3" points="131.09 0 94.38 0 0 201.35 20.97 239.11 131.09 0"/><ellipse class="cls-4" cx="114.95" cy="283.36" rx="82.83" ry="7.68"/><path class="cls-2" d="M290.79,131.24H344v14.32h-35.6v27.38h32.06V187.4H308.37v28.79H344v14.33H290.79Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M353.19,220.59l15.46-27-13.9-24.11V159.6h13.33l14.75,27.66,14.75-27.66h13.33v9.93L397,193.64l15.46,27v9.93H399.14L382.83,200l-16.31,30.49H353.19Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M426.93,172.94h-9.5V159.6h9.5V144h19.15L438,159.6h20v13.34H443.39V211.8a5.11,5.11,0,0,0,5.39,5.39H458v13.33H445.66q-8.37,0-13.55-5.18t-5.18-13.54Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M475,167.05q8.65-8.73,23-8.72,14.17,0,22.83,8.72T529.48,190v10.21q0,14.18-8.66,22.9T498,231.79q-14.33,0-23-8.72t-8.65-22.9V190Q466.36,175.77,475,167.05Zm12,46.24a14.85,14.85,0,0,0,11,4.18q6.81,0,10.92-4.18A14.83,14.83,0,0,0,513,202.44V187.69q0-6.81-4.11-10.93T498,172.65a15,15,0,0,0-11,4.11q-4.19,4.13-4.19,10.93v14.75A14.69,14.69,0,0,0,487,213.29Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M541.53,127.69H558V230.52H541.53Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M573,127.69h16.45V230.52H573Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M610,167.05q8.64-8.73,23-8.72t22.84,8.72q8.65,8.72,8.65,22.91v10.21q0,14.18-8.65,22.9T633,231.79q-14.32,0-23-8.72t-8.65-22.9V190Q601.38,175.77,610,167.05Zm12,46.24a14.85,14.85,0,0,0,11,4.18q6.81,0,10.92-4.18A14.8,14.8,0,0,0,648,202.44V187.69q0-6.81-4.12-10.93T633,172.65a15,15,0,0,0-11,4.11q-4.18,4.13-4.18,10.93v14.75A14.68,14.68,0,0,0,622,213.29Z" transform="translate(-18 -34.48)"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,17 @@
doctype html
html
head
meta(charset='utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1')
block head
block styles
block prescripts
script.
if (typeof $ !== 'function') {
window.$ = (...args) => document.querySelector(...args)
}
script(src=vendor('@extollo/ui', 'extollo-ui.dist.js') type='module')
body
ex-page
block content
block scripts

View File

@ -0,0 +1,18 @@
extends base
block content
ex-nav
img(src=vendor('@extollo/lib', 'extollo.svg') slot='branding' width=115 style="margin-right: 30px" alt=config('app.name'))
ex-nav-item(title='Home' name='home' href=route('/'))
ex-nav-item(title='Documentation' name='documentation' href='https://extollo.garrettmills.dev')
if hasRoute('@auth.login')
if user()
ex-nav-item(title='Welcome, ' + user().getDisplay() name='user' href=named('@auth.logout') right)
else
ex-nav-item(title='Login' name='login' href=named('@auth.login') right)
h1 Welcome to #{config('app.name')}
p You have successfully created your new application, #{config('app.name')}, based on the Extollo framework.
p For more information on Extollo, visit the framework documentation using the link above.
blockquote You can customize this view by modifying <code>src/app/resources/views/welcome.pug</code>.

View File

@ -19,6 +19,7 @@ import {Config} from './Config'
import {InjectRequestEventBusHTTPModule} from '../http/kernel/module/InjectRequestEventBusHTTPModule'
import {Routing} from './Routing'
import {EventBus} from '../event/EventBus'
import {RequestLocalStorage} from '../http/RequestLocalStorage'
/**
* Application unit that starts the HTTP/S server, creates Request and Response objects
@ -41,6 +42,9 @@ export class HTTPServer extends Unit {
@Inject()
protected readonly bus!: EventBus
@Inject()
protected readonly requestLocalStorage!: RequestLocalStorage
/** The underlying native Node.js server. */
protected server?: Server
@ -88,40 +92,42 @@ export class HTTPServer extends Unit {
return async (request: IncomingMessage, response: ServerResponse) => {
const extolloReq = new Request(request, response)
withTimeout(timeout, extolloReq.response.sent$.toPromise())
.onTime(() => {
this.logging.verbose(`Request lifecycle finished on time. (Path: ${extolloReq.path})`)
})
.late(() => {
if ( !extolloReq.bypassTimeout ) {
this.logging.warn(`Request lifecycle finished late, so an error response was returned! (Path: ${extolloReq.path})`)
}
})
.timeout(() => {
if ( extolloReq.bypassTimeout ) {
this.logging.info(`Request lifecycle has timed out, but bypassRequest was set. (Path: ${extolloReq.path})`)
return
await this.requestLocalStorage.run(extolloReq, async () => {
withTimeout(timeout, extolloReq.response.sent$.toPromise())
.onTime(() => {
this.logging.verbose(`Request lifecycle finished on time. (Path: ${extolloReq.path})`)
})
.late(() => {
if ( !extolloReq.bypassTimeout ) {
this.logging.warn(`Request lifecycle finished late, so an error response was returned! (Path: ${extolloReq.path})`)
}
})
.timeout(() => {
if ( extolloReq.bypassTimeout ) {
this.logging.info(`Request lifecycle has timed out, but bypassRequest was set. (Path: ${extolloReq.path})`)
return
}
this.logging.error(`Request lifecycle has timed out. Will send error response instead. (Path: ${extolloReq.path})`)
extolloReq.response.setStatus(HTTPStatus.REQUEST_TIMEOUT)
extolloReq.response.body = 'Sorry, your request timed out.'
extolloReq.response.send()
})
.run()
.catch(e => this.logging.error(e))
try {
await this.kernel.handle(extolloReq)
} catch (e) {
if ( e instanceof Error ) {
await error(e).write(extolloReq)
}
this.logging.error(`Request lifecycle has timed out. Will send error response instead. (Path: ${extolloReq.path})`)
extolloReq.response.setStatus(HTTPStatus.REQUEST_TIMEOUT)
extolloReq.response.body = 'Sorry, your request timed out.'
extolloReq.response.send()
})
.run()
.catch(e => this.logging.error(e))
try {
await this.kernel.handle(extolloReq)
} catch (e) {
if ( e instanceof Error ) {
await error(e).write(extolloReq)
await error(new ErrorWithContext('Unknown error occurred.', { e }))
}
await error(new ErrorWithContext('Unknown error occurred.', { e }))
}
await extolloReq.response.send()
await extolloReq.response.send()
})
}
}
}

View File

@ -1,8 +1,10 @@
import {AppClass} from '../lifecycle/AppClass'
import {Config} from '../service/Config'
import {Container} from '../di'
import {ErrorWithContext, UniversalPath} from '../util'
import {ErrorWithContext, hasOwnProperty, Maybe, UniversalPath} from '../util'
import {Routing} from '../service/Routing'
import {RequestLocalStorage} from '../http/RequestLocalStorage'
import {Request} from '../http/lifecycle/Request'
/**
* Abstract base class for rendering views via different view engines.
@ -12,14 +14,19 @@ export abstract class ViewEngine extends AppClass {
protected readonly routing: Routing
protected readonly request: RequestLocalStorage
protected readonly debug: boolean
protected readonly namespaces: {[key: string]: UniversalPath} = {}
protected readonly globals: {[key: string]: (req: Maybe<Request>) => any} = {}
constructor() {
super()
this.config = Container.getContainer().make(Config)
this.routing = Container.getContainer().make(Routing)
this.request = Container.getContainer().make(RequestLocalStorage)
this.debug = (this.config.get('server.mode', 'production') === 'development'
|| this.config.get('server.debug', false))
}
@ -56,7 +63,7 @@ export abstract class ViewEngine extends AppClass {
* @protected
*/
protected getGlobals(): {[key: string]: any} {
return {
const globals: {[key: string]: any} = {
app: this.app(),
config: (key: string, fallback?: any) => this.config.get(key, fallback),
asset: (...parts: string[]) => this.routing.getAssetPath(...parts).toRemote,
@ -65,6 +72,31 @@ export abstract class ViewEngine extends AppClass {
route: (...parts: string[]) => this.routing.getAppUrl().concat(...parts).toRemote,
hasRoute: (name: string) => this.routing.hasNamedRoute(name),
}
const req = this.request.get()
if ( req ) {
globals.request = () => req
}
for ( const key in this.globals ) {
if ( !hasOwnProperty(this.globals, key) ) {
continue
}
globals[key] = this.globals[key](req)
}
return globals
}
/**
* Register a new factory that produces a global available in the templates by default.
* @param name
* @param factory
*/
public registerGlobalFactory(name: string, factory: (req: Maybe<Request>) => any): this {
this.globals[name] = factory
return this
}
/**