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
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
b5eb407b55
commit
463076d182
@ -7,6 +7,8 @@ import {Middleware} from '../http/routing/Middleware'
|
|||||||
import {AuthRequiredMiddleware} from './middleware/AuthRequiredMiddleware'
|
import {AuthRequiredMiddleware} from './middleware/AuthRequiredMiddleware'
|
||||||
import {GuestRequiredMiddleware} from './middleware/GuestRequiredMiddleware'
|
import {GuestRequiredMiddleware} from './middleware/GuestRequiredMiddleware'
|
||||||
import {SessionAuthMiddleware} from './middleware/SessionAuthMiddleware'
|
import {SessionAuthMiddleware} from './middleware/SessionAuthMiddleware'
|
||||||
|
import {ViewEngine} from '../views/ViewEngine'
|
||||||
|
import {SecurityContext} from './context/SecurityContext'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Authentication extends Unit {
|
export class Authentication extends Unit {
|
||||||
@ -18,6 +20,13 @@ export class Authentication extends Unit {
|
|||||||
|
|
||||||
async up(): Promise<void> {
|
async up(): Promise<void> {
|
||||||
this.middleware.registerNamespace('@auth', this.getMiddlewareResolver())
|
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>> {
|
protected getMiddlewareResolver(): CanonicalResolver<StaticInstantiable<Middleware>> {
|
||||||
|
32
src/http/RequestLocalStorage.ts
Normal file
32
src/http/RequestLocalStorage.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@ export * from './http/kernel/HTTPCookieJar'
|
|||||||
|
|
||||||
export * from './http/lifecycle/Request'
|
export * from './http/lifecycle/Request'
|
||||||
export * from './http/lifecycle/Response'
|
export * from './http/lifecycle/Response'
|
||||||
|
export * from './http/RequestLocalStorage'
|
||||||
|
|
||||||
export * as api from './http/response/api'
|
export * as api from './http/response/api'
|
||||||
export * from './http/response/DehydratedStateResponseFactory'
|
export * from './http/response/DehydratedStateResponseFactory'
|
||||||
|
1
src/resources/assets/extollo.svg
Normal file
1
src/resources/assets/extollo.svg
Normal 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 |
17
src/resources/views/base.pug
Normal file
17
src/resources/views/base.pug
Normal 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
|
18
src/resources/views/welcome.pug
Normal file
18
src/resources/views/welcome.pug
Normal 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>.
|
@ -19,6 +19,7 @@ import {Config} from './Config'
|
|||||||
import {InjectRequestEventBusHTTPModule} from '../http/kernel/module/InjectRequestEventBusHTTPModule'
|
import {InjectRequestEventBusHTTPModule} from '../http/kernel/module/InjectRequestEventBusHTTPModule'
|
||||||
import {Routing} from './Routing'
|
import {Routing} from './Routing'
|
||||||
import {EventBus} from '../event/EventBus'
|
import {EventBus} from '../event/EventBus'
|
||||||
|
import {RequestLocalStorage} from '../http/RequestLocalStorage'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application unit that starts the HTTP/S server, creates Request and Response objects
|
* Application unit that starts the HTTP/S server, creates Request and Response objects
|
||||||
@ -41,6 +42,9 @@ export class HTTPServer extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly bus!: EventBus
|
protected readonly bus!: EventBus
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly requestLocalStorage!: RequestLocalStorage
|
||||||
|
|
||||||
/** The underlying native Node.js server. */
|
/** The underlying native Node.js server. */
|
||||||
protected server?: Server
|
protected server?: Server
|
||||||
|
|
||||||
@ -88,6 +92,7 @@ export class HTTPServer extends Unit {
|
|||||||
return async (request: IncomingMessage, response: ServerResponse) => {
|
return async (request: IncomingMessage, response: ServerResponse) => {
|
||||||
const extolloReq = new Request(request, response)
|
const extolloReq = new Request(request, response)
|
||||||
|
|
||||||
|
await this.requestLocalStorage.run(extolloReq, async () => {
|
||||||
withTimeout(timeout, extolloReq.response.sent$.toPromise())
|
withTimeout(timeout, extolloReq.response.sent$.toPromise())
|
||||||
.onTime(() => {
|
.onTime(() => {
|
||||||
this.logging.verbose(`Request lifecycle finished on time. (Path: ${extolloReq.path})`)
|
this.logging.verbose(`Request lifecycle finished on time. (Path: ${extolloReq.path})`)
|
||||||
@ -122,6 +127,7 @@ export class HTTPServer extends Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await extolloReq.response.send()
|
await extolloReq.response.send()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import {AppClass} from '../lifecycle/AppClass'
|
import {AppClass} from '../lifecycle/AppClass'
|
||||||
import {Config} from '../service/Config'
|
import {Config} from '../service/Config'
|
||||||
import {Container} from '../di'
|
import {Container} from '../di'
|
||||||
import {ErrorWithContext, UniversalPath} from '../util'
|
import {ErrorWithContext, hasOwnProperty, Maybe, UniversalPath} from '../util'
|
||||||
import {Routing} from '../service/Routing'
|
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.
|
* 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 routing: Routing
|
||||||
|
|
||||||
|
protected readonly request: RequestLocalStorage
|
||||||
|
|
||||||
protected readonly debug: boolean
|
protected readonly debug: boolean
|
||||||
|
|
||||||
protected readonly namespaces: {[key: string]: UniversalPath} = {}
|
protected readonly namespaces: {[key: string]: UniversalPath} = {}
|
||||||
|
|
||||||
|
protected readonly globals: {[key: string]: (req: Maybe<Request>) => any} = {}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.config = Container.getContainer().make(Config)
|
this.config = Container.getContainer().make(Config)
|
||||||
this.routing = Container.getContainer().make(Routing)
|
this.routing = Container.getContainer().make(Routing)
|
||||||
|
this.request = Container.getContainer().make(RequestLocalStorage)
|
||||||
this.debug = (this.config.get('server.mode', 'production') === 'development'
|
this.debug = (this.config.get('server.mode', 'production') === 'development'
|
||||||
|| this.config.get('server.debug', false))
|
|| this.config.get('server.debug', false))
|
||||||
}
|
}
|
||||||
@ -56,7 +63,7 @@ export abstract class ViewEngine extends AppClass {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected getGlobals(): {[key: string]: any} {
|
protected getGlobals(): {[key: string]: any} {
|
||||||
return {
|
const globals: {[key: string]: any} = {
|
||||||
app: this.app(),
|
app: this.app(),
|
||||||
config: (key: string, fallback?: any) => this.config.get(key, fallback),
|
config: (key: string, fallback?: any) => this.config.get(key, fallback),
|
||||||
asset: (...parts: string[]) => this.routing.getAssetPath(...parts).toRemote,
|
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,
|
route: (...parts: string[]) => this.routing.getAppUrl().concat(...parts).toRemote,
|
||||||
hasRoute: (name: string) => this.routing.hasNamedRoute(name),
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user