Named routes & basic login framework
This commit is contained in:
parent
e33d8dee8f
commit
e86cf420df
24
src/auth/basic-ui/BasicLoginFormRequest.ts
Normal file
24
src/auth/basic-ui/BasicLoginFormRequest.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {FormRequest, ValidationRules} from '../../forms'
|
||||
import {Is, Str} from '../../forms/rules/rules'
|
||||
import {Singleton} from '../../di'
|
||||
|
||||
export interface BasicLoginCredentials {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
@Singleton()
|
||||
export class BasicLoginFormRequest extends FormRequest<BasicLoginCredentials> {
|
||||
protected getRules(): ValidationRules {
|
||||
return {
|
||||
username: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
password: [
|
||||
Is.required,
|
||||
Str.lengthMin(1),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
@ -19,3 +19,5 @@ export * from './middleware/SessionAuthMiddleware'
|
||||
export * from './Authentication'
|
||||
|
||||
export * from './config'
|
||||
|
||||
export * from './basic-ui/BasicLoginFormRequest'
|
||||
|
@ -5,15 +5,30 @@ import {ResponseObject} from '../../http/routing/Route'
|
||||
import {error} from '../../http/response/ErrorResponseFactory'
|
||||
import {NotAuthorizedError} from '../NotAuthorizedError'
|
||||
import {HTTPStatus} from '../../util'
|
||||
import {redirect} from '../../http/response/RedirectResponseFactory'
|
||||
import {Routing} from '../../service/Routing'
|
||||
import {Session} from '../../http/session/Session'
|
||||
|
||||
@Injectable()
|
||||
export class AuthRequiredMiddleware extends Middleware {
|
||||
@Inject()
|
||||
protected readonly security!: SecurityContext
|
||||
|
||||
@Inject()
|
||||
protected readonly routing!: Routing
|
||||
|
||||
@Inject()
|
||||
protected readonly session!: Session
|
||||
|
||||
async apply(): Promise<ResponseObject> {
|
||||
if ( !this.security.hasUser() ) {
|
||||
return error(new NotAuthorizedError(), HTTPStatus.FORBIDDEN)
|
||||
this.session.set('auth.intention', this.request.url)
|
||||
|
||||
if ( this.routing.hasNamedRoute('@auth.login') ) {
|
||||
return redirect(this.routing.getNamedPath('@auth.login').toRemote)
|
||||
} else {
|
||||
return error(new NotAuthorizedError(), HTTPStatus.FORBIDDEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,24 @@ import {ResponseObject} from '../../http/routing/Route'
|
||||
import {error} from '../../http/response/ErrorResponseFactory'
|
||||
import {NotAuthorizedError} from '../NotAuthorizedError'
|
||||
import {HTTPStatus} from '../../util'
|
||||
import {Routing} from '../../service/Routing'
|
||||
import {redirect} from '../../http/response/RedirectResponseFactory'
|
||||
|
||||
@Injectable()
|
||||
export class GuestRequiredMiddleware extends Middleware {
|
||||
@Inject()
|
||||
protected readonly security!: SecurityContext
|
||||
|
||||
@Inject()
|
||||
protected readonly routing!: Routing
|
||||
|
||||
async apply(): Promise<ResponseObject> {
|
||||
if ( this.security.hasUser() ) {
|
||||
return error(new NotAuthorizedError(), HTTPStatus.FORBIDDEN)
|
||||
if ( this.routing.hasNamedRoute('@auth.redirectFromGuest') ) {
|
||||
return redirect(this.routing.getNamedPath('@auth.redirectFromGuest').toRemote)
|
||||
} else {
|
||||
return error(new NotAuthorizedError(), HTTPStatus.FORBIDDEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {Logging} from '../../service/Logging'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {error} from '../response/ErrorResponseFactory'
|
||||
import {HTTPError} from '../HTTPError'
|
||||
|
||||
/**
|
||||
* Interface for fluently registering kernel modules into the kernel.
|
||||
@ -105,7 +106,8 @@ export class HTTPKernel extends AppClass {
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logging.error(e)
|
||||
await error(e).status(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
const status = (e instanceof HTTPError && e.status) ? e.status : HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
await error(e).status(status)
|
||||
.write(request)
|
||||
}
|
||||
|
||||
|
@ -138,7 +138,6 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
})
|
||||
|
||||
busboy.on('finish', () => {
|
||||
this.logging.debug(`Parsed body input: ${JSON.stringify(request.parsedInput)}`)
|
||||
res()
|
||||
})
|
||||
|
||||
|
@ -21,6 +21,7 @@ export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( !this.config.get('server.poweredBy.hide', false) ) {
|
||||
request.response.setHeader('X-Powered-By', this.config.get('server.poweredBy.header', 'Extollo'))
|
||||
request.response.setHeader('Server', this.config.get('server.poweredBy.header', 'Extollo'))
|
||||
}
|
||||
|
||||
return request
|
||||
|
@ -229,6 +229,9 @@ export class Response {
|
||||
this.setHeader('Content-Length', String(this.body?.length ?? 0))
|
||||
}
|
||||
|
||||
this.setHeader('Date', (new Date()).toUTCString())
|
||||
this.setHeader('Permissions-Policy', 'interest-cohort=()')
|
||||
|
||||
await this.write(this.body ?? '')
|
||||
this.end()
|
||||
|
||||
|
31
src/http/response/RedirectResponseFactory.ts
Normal file
31
src/http/response/RedirectResponseFactory.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {HTTPStatus} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function to create a new RedirectResponseFactory to the given destination.
|
||||
* @param destination
|
||||
*/
|
||||
export function redirect(destination: string): RedirectResponseFactory {
|
||||
return new RedirectResponseFactory(destination)
|
||||
}
|
||||
|
||||
/**
|
||||
* Response factory that sends an HTTP redirect to the given destination.
|
||||
*/
|
||||
export class RedirectResponseFactory extends ResponseFactory {
|
||||
protected targetStatus: HTTPStatus = HTTPStatus.MOVED_TEMPORARILY
|
||||
|
||||
constructor(
|
||||
/** THe URL where the client should redirect to. */
|
||||
public readonly destination: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Location', this.destination)
|
||||
return request
|
||||
}
|
||||
}
|
40
src/http/response/RouteResponseFactory.ts
Normal file
40
src/http/response/RouteResponseFactory.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {HTTPStatus} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {Routing} from '../../service/Routing'
|
||||
|
||||
/**
|
||||
* Helper function to create a new RouteResponseFactory to the given destination.
|
||||
* @param nameOrPath
|
||||
*/
|
||||
export function route(nameOrPath: string): RouteResponseFactory {
|
||||
return new RouteResponseFactory(nameOrPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Response factory that sends an HTTP redirect to the given destination.
|
||||
*/
|
||||
export class RouteResponseFactory extends ResponseFactory {
|
||||
protected targetStatus: HTTPStatus = HTTPStatus.MOVED_TEMPORARILY
|
||||
|
||||
constructor(
|
||||
/** The alias or path of the route to redirect to. */
|
||||
public readonly nameOrPath: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request): Promise<Request> {
|
||||
const routing = <Routing> request.make(Routing)
|
||||
request = await super.write(request)
|
||||
|
||||
try {
|
||||
const routePath = routing.getNamedPath(this.nameOrPath)
|
||||
request.response.setHeader('Location', routePath.toRemote)
|
||||
} catch (e: unknown) {
|
||||
request.response.setHeader('Location', routing.getAppUrl().concat(this.nameOrPath).toRemote)
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import {Request} from '../lifecycle/Request'
|
||||
* Helper function to create a new TemporaryRedirectResponseFactory to the given destination.
|
||||
* @param destination
|
||||
*/
|
||||
export function redirect(destination: string): TemporaryRedirectResponseFactory {
|
||||
export function temporary(destination: string): TemporaryRedirectResponseFactory {
|
||||
return new TemporaryRedirectResponseFactory(destination)
|
||||
}
|
||||
|
||||
|
@ -215,6 +215,9 @@ export class Route extends AppClass {
|
||||
/** Pre-compiled route handler for the main route handler for this route. */
|
||||
protected compiledPostflight?: ResolvedRouteHandler[]
|
||||
|
||||
/** Programmatic aliases of this route. */
|
||||
public aliases: string[] = []
|
||||
|
||||
constructor(
|
||||
/** The HTTP method(s) that this route listens on. */
|
||||
protected method: HTTPMethod | HTTPMethod[],
|
||||
@ -228,6 +231,15 @@ export class Route extends AppClass {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a programmatic name for this route.
|
||||
* @param name
|
||||
*/
|
||||
public alias(name: string): this {
|
||||
this.aliases.push(name)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string-form of the route.
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@ import {Collection, HTTPStatus, UniversalPath, universalPath} from '../../util'
|
||||
import {Application} from '../../lifecycle/Application'
|
||||
import {HTTPError} from '../HTTPError'
|
||||
import {view, ViewResponseFactory} from '../response/ViewResponseFactory'
|
||||
import {redirect} from '../response/TemporaryRedirectResponseFactory'
|
||||
import {redirect} from '../response/RedirectResponseFactory'
|
||||
import {file} from '../response/FileResponseFactory'
|
||||
import {RouteHandler} from '../routing/Route'
|
||||
|
||||
@ -24,6 +24,9 @@ export interface StaticServerOptions {
|
||||
|
||||
/** If specified, files with these extensions will not be served. */
|
||||
excludedExtensions?: string[]
|
||||
|
||||
/** If a file with this name exists in a directory, it will be served. */
|
||||
indexFile?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,6 +159,13 @@ export function staticServer(options: StaticServerOptions = {}): RouteHandler {
|
||||
|
||||
// If the resolved path is a directory, send the directory listing response
|
||||
if ( await filePath.isDirectory() ) {
|
||||
if ( options.indexFile ) {
|
||||
const indexFile = filePath.concat(options.indexFile)
|
||||
if ( await indexFile.exists() ) {
|
||||
return file(indexFile)
|
||||
}
|
||||
}
|
||||
|
||||
if ( !options.directoryListing ) {
|
||||
throw new StaticServerHTTPError(HTTPStatus.NOT_FOUND, 'File not found', {
|
||||
basePath: basePath.toString(),
|
||||
|
@ -93,4 +93,12 @@ export class MemorySession extends Session {
|
||||
|
||||
this.data[key] = value
|
||||
}
|
||||
|
||||
public forget(key: string): void {
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
delete this.data[key]
|
||||
}
|
||||
}
|
||||
|
@ -57,4 +57,7 @@ export abstract class Session {
|
||||
|
||||
/** Set a value in the session by key. */
|
||||
public abstract set(key: string, value: unknown): void
|
||||
|
||||
/** Remove a key from the session data. */
|
||||
public abstract forget(key: string): void
|
||||
}
|
||||
|
@ -42,8 +42,10 @@ export * from './http/response/JSONResponseFactory'
|
||||
export * from './http/response/ResponseFactory'
|
||||
export * from './http/response/StringResponseFactory'
|
||||
export * from './http/response/TemporaryRedirectResponseFactory'
|
||||
export * from './http/response/RedirectResponseFactory'
|
||||
export * from './http/response/ViewResponseFactory'
|
||||
export * from './http/response/FileResponseFactory'
|
||||
export * from './http/response/RouteResponseFactory'
|
||||
|
||||
export * from './http/routing/ActivatedRoute'
|
||||
export * from './http/routing/Route'
|
||||
|
@ -68,6 +68,7 @@ export class ORMSession extends Session {
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
return this.data[key] ?? fallback
|
||||
}
|
||||
|
||||
@ -75,6 +76,15 @@ export class ORMSession extends Session {
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
this.data[key] = value
|
||||
}
|
||||
|
||||
public forget(key: string): void {
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
delete this.data[key]
|
||||
}
|
||||
}
|
||||
|
116
src/resources/assets/auth/theme.css
Normal file
116
src/resources/assets/auth/theme.css
Normal file
@ -0,0 +1,116 @@
|
||||
:root {
|
||||
--input-padding-x: 1.5rem;
|
||||
--input-padding-y: 0.75rem;
|
||||
}
|
||||
|
||||
.login,
|
||||
.image {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.login-heading {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.05rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.form-label-group {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label-group>input,
|
||||
.form-label-group>label {
|
||||
padding: var(--input-padding-y) var(--input-padding-x);
|
||||
height: auto;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.form-label-group>label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
/* Override default `<label>` margin */
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
cursor: text;
|
||||
/* Match the input under the label */
|
||||
border: 1px solid transparent;
|
||||
border-radius: .25rem;
|
||||
transition: all .1s ease-in-out;
|
||||
}
|
||||
|
||||
.form-label-group input::-webkit-input-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.form-label-group input:-ms-input-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.form-label-group input::-ms-input-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.form-label-group input::-moz-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.form-label-group input::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.form-label-group input:not(:placeholder-shown) {
|
||||
padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
|
||||
padding-bottom: calc(var(--input-padding-y) / 3);
|
||||
}
|
||||
|
||||
.form-label-group input:not(:placeholder-shown)~label {
|
||||
padding-top: calc(var(--input-padding-y) / 3);
|
||||
padding-bottom: calc(var(--input-padding-y) / 3);
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.form-error-message {
|
||||
color: darkred;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-submit-button {
|
||||
margin-top: 50px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* Fallback for Edge
|
||||
-------------------------------------------------- */
|
||||
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.form-label-group>label {
|
||||
display: none;
|
||||
}
|
||||
.form-label-group input::-ms-input-placeholder {
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for IE
|
||||
-------------------------------------------------- */
|
||||
|
||||
@media all and (-ms-high-contrast: none),
|
||||
(-ms-high-contrast: active) {
|
||||
.form-label-group>label {
|
||||
display: none;
|
||||
}
|
||||
.form-label-group input:-ms-input-placeholder {
|
||||
color: #777;
|
||||
}
|
||||
}
|
7
src/resources/assets/lib/bootstrap.min.css
vendored
Normal file
7
src/resources/assets/lib/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
src/resources/assets/lib/bootstrap.min.js
vendored
Normal file
7
src/resources/assets/lib/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -4,19 +4,21 @@ block head
|
||||
title Login | #{config('app.name', 'Extollo')}
|
||||
|
||||
block heading
|
||||
| Login to Continue
|
||||
| Login to continue
|
||||
|
||||
block form
|
||||
.form-label-group
|
||||
input#inputUsername.form-control(type='text' name='username' value=(form_data ? form_data.username : '') required placeholder='Username' autofocus)
|
||||
input#inputUsername.form-control(type='text' name='username' 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')
|
||||
label(for='inputPassword') 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 Need an account?
|
||||
a(href='./register') Register here.
|
||||
// .text-center
|
||||
span.small(style="color: #999999;") Provider: #{provider_name}
|
||||
|
12
src/resources/views/auth/message.pug
Normal file
12
src/resources/views/auth/message.pug
Normal file
@ -0,0 +1,12 @@
|
||||
extends ./theme
|
||||
|
||||
block content
|
||||
if heading
|
||||
h3.login-heading.mb-4 #{heading}
|
||||
|
||||
if errors
|
||||
each error in errors
|
||||
p.form-error-message #{error}
|
||||
|
||||
if message
|
||||
p #{message}
|
@ -5,18 +5,17 @@ html
|
||||
block head
|
||||
|
||||
block styles
|
||||
link(rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css')
|
||||
link(rel='stylesheet' href=vendor('@extollo', 'auth/theme.css'))
|
||||
link(rel='stylesheet' href=vendor('@extollo/lib', 'lib/bootstrap.min.css'))
|
||||
link(rel='stylesheet' href=vendor('@extollo/lib', 'auth/theme.css'))
|
||||
body
|
||||
.container-fluid
|
||||
.row.no-gutter
|
||||
.d-none.d-md-flex.col-md-6.col-lg-8.bg-image
|
||||
.col-md-6.col-lg-4
|
||||
.col-md-12.col-lg-12
|
||||
.login.d-flex.align-items-center.py-5
|
||||
.container
|
||||
.row
|
||||
.col-md-9.col-lg-8.mx-auto
|
||||
.col-md-9.col-lg-6.mx-auto
|
||||
block content
|
||||
|
||||
block scripts
|
||||
script(src='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css')
|
||||
script(src=vendor('@extollo/lib', 'lib/bootstrap.min.js'))
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Inject, Singleton} from '../di'
|
||||
import {HTTPStatus, universalPath, UniversalPath, withTimeout} from '../util'
|
||||
import {HTTPStatus, withTimeout} from '../util'
|
||||
import {Unit} from '../lifecycle/Unit'
|
||||
import {createServer, IncomingMessage, RequestListener, Server, ServerResponse} from 'http'
|
||||
import {Logging} from './Logging'
|
||||
@ -18,10 +18,7 @@ import {ParseIncomingBodyHTTPModule} from '../http/kernel/module/ParseIncomingBo
|
||||
import {Config} from './Config'
|
||||
import {InjectRequestEventBusHTTPModule} from '../http/kernel/module/InjectRequestEventBusHTTPModule'
|
||||
import {Routing} from './Routing'
|
||||
import {Route} from '../http/routing/Route'
|
||||
import {staticServer} from '../http/servers/static'
|
||||
import {EventBus} from '../event/EventBus'
|
||||
import {PackageDiscovered} from '../support/PackageDiscovered'
|
||||
|
||||
/**
|
||||
* Application unit that starts the HTTP/S server, creates Request and Response objects
|
||||
@ -47,30 +44,6 @@ export class HTTPServer extends Unit {
|
||||
/** The underlying native Node.js server. */
|
||||
protected server?: Server
|
||||
|
||||
/**
|
||||
* Register an asset directory for the given package.
|
||||
* This creates a static server route for the package with the
|
||||
* configured vendor prefix.
|
||||
* @param packageName
|
||||
* @param basePath
|
||||
*/
|
||||
public async registerVendorAssets(packageName: string, basePath: UniversalPath): Promise<void> {
|
||||
if ( this.config.get('server.builtIns.vendor.enabled', true) ) {
|
||||
this.logging.debug(`Registering vendor assets route for package ${packageName} on ${basePath}...`)
|
||||
await this.routing.registerRoutes(() => {
|
||||
const prefix = this.config.get('server.builtIns.vendor.prefix', '/vendor')
|
||||
Route.group(prefix, () => {
|
||||
Route.group(packageName, () => {
|
||||
Route.get('/**', staticServer({
|
||||
basePath,
|
||||
directoryListing: false,
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async up(): Promise<void> {
|
||||
const port = this.config.get('server.port', 8000)
|
||||
|
||||
@ -86,8 +59,6 @@ export class HTTPServer extends Unit {
|
||||
ParseIncomingBodyHTTPModule.register(this.kernel)
|
||||
InjectRequestEventBusHTTPModule.register(this.kernel)
|
||||
|
||||
await this.registerBuiltIns()
|
||||
|
||||
await new Promise<void>(res => {
|
||||
this.server = createServer(this.handler)
|
||||
|
||||
@ -148,34 +119,4 @@ export class HTTPServer extends Unit {
|
||||
await extolloReq.response.send()
|
||||
}
|
||||
}
|
||||
|
||||
/** Register built-in servers and routes. */
|
||||
protected async registerBuiltIns(): Promise<void> {
|
||||
const extolloAssets = universalPath(__dirname, '..', 'resources', 'assets')
|
||||
await this.registerVendorAssets('@extollo/lib', extolloAssets)
|
||||
|
||||
this.bus.subscribe(PackageDiscovered, async (event: PackageDiscovered) => {
|
||||
if ( event.packageConfig?.extollo?.assets?.discover && event.packageConfig.name ) {
|
||||
this.logging.debug(`Registering vendor assets for discovered package: ${event.packageConfig.name}`)
|
||||
const basePath = event.packageConfig?.extollo?.assets?.basePath
|
||||
if ( basePath && Array.isArray(basePath) ) {
|
||||
const assetPath = event.packageJson.concat('..', ...basePath)
|
||||
await this.registerVendorAssets(event.packageConfig.name, assetPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if ( this.config.get('server.builtIns.assets.enabled', true) ) {
|
||||
const prefix = this.config.get('server.builtIns.assets.prefix', '/assets')
|
||||
this.logging.debug(`Registering built-in assets server with prefix: ${prefix}`)
|
||||
await this.routing.registerRoutes(() => {
|
||||
Route.group(prefix, () => {
|
||||
Route.get('/**', staticServer({
|
||||
directoryListing: false,
|
||||
basePath: ['resources', 'assets'],
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Inject, Singleton} from '../di'
|
||||
import {Awaitable, Collection, Pipe, universalPath, UniversalPath} from '../util'
|
||||
import {Awaitable, Collection, ErrorWithContext, Maybe, Pipe, universalPath, UniversalPath} from '../util'
|
||||
import {Unit, UnitStatus} from '../lifecycle/Unit'
|
||||
import {Logging} from './Logging'
|
||||
import {Route} from '../http/routing/Route'
|
||||
@ -10,6 +10,7 @@ import {lib} from '../lib'
|
||||
import {Config} from './Config'
|
||||
import {EventBus} from '../event/EventBus'
|
||||
import {PackageDiscovered} from '../support/PackageDiscovered'
|
||||
import {staticServer} from '../http/servers/static'
|
||||
|
||||
/**
|
||||
* Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers.
|
||||
@ -43,6 +44,8 @@ export class Routing extends Unit {
|
||||
await import(entry)
|
||||
}
|
||||
|
||||
await this.registerBuiltIns()
|
||||
|
||||
this.logging.info('Compiling routes...')
|
||||
this.compiledRoutes = new Collection<Route>(await Route.compile())
|
||||
|
||||
@ -140,6 +143,26 @@ export class Routing extends Unit {
|
||||
return this.getAppUrl().concat(this.config.get('server.builtIns.vendor.prefix', '/vendor'))
|
||||
}
|
||||
|
||||
public getNamedPath(name: string): UniversalPath {
|
||||
const route = this.getByName(name)
|
||||
if ( route ) {
|
||||
return this.getAppUrl().concat(route.getRoute())
|
||||
}
|
||||
|
||||
throw new ErrorWithContext(`Route does not exist with name: ${name}`, {
|
||||
routeName: name,
|
||||
})
|
||||
}
|
||||
|
||||
public hasNamedRoute(name: string): boolean {
|
||||
return Boolean(this.getByName(name))
|
||||
}
|
||||
|
||||
public getByName(name: string): Maybe<Route> {
|
||||
return this.compiledRoutes
|
||||
.firstWhere(route => route.aliases.includes(name))
|
||||
}
|
||||
|
||||
public getAppUrl(): UniversalPath {
|
||||
const rawHost = String(this.config.get('server.url', 'http://localhost')).toLowerCase()
|
||||
const isSSL = rawHost.startsWith('https://')
|
||||
@ -166,4 +189,58 @@ export class Routing extends Unit {
|
||||
.tap<UniversalPath>(host => universalPath(host))
|
||||
.get()
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an asset directory for the given package.
|
||||
* This creates a static server route for the package with the
|
||||
* configured vendor prefix.
|
||||
* @param packageName
|
||||
* @param basePath
|
||||
*/
|
||||
public async registerVendorAssets(packageName: string, basePath: UniversalPath): Promise<void> {
|
||||
if ( this.config.get('server.builtIns.vendor.enabled', true) ) {
|
||||
this.logging.debug(`Registering vendor assets route for package ${packageName} on ${basePath}...`)
|
||||
await this.registerRoutes(() => {
|
||||
const prefix = this.config.get('server.builtIns.vendor.prefix', '/vendor')
|
||||
Route.group(prefix, () => {
|
||||
Route.group(packageName, () => {
|
||||
Route.get('/**', staticServer({
|
||||
basePath,
|
||||
directoryListing: false,
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** Register built-in servers and routes. */
|
||||
protected async registerBuiltIns(): Promise<void> {
|
||||
const extolloAssets = universalPath(__dirname, '..', 'resources', 'assets')
|
||||
await this.registerVendorAssets('@extollo/lib', extolloAssets)
|
||||
|
||||
this.bus.subscribe(PackageDiscovered, async (event: PackageDiscovered) => {
|
||||
if ( event.packageConfig?.extollo?.assets?.discover && event.packageConfig.name ) {
|
||||
this.logging.debug(`Registering vendor assets for discovered package: ${event.packageConfig.name}`)
|
||||
const basePath = event.packageConfig?.extollo?.assets?.basePath
|
||||
if ( basePath && Array.isArray(basePath) ) {
|
||||
const assetPath = event.packageJson.concat('..', ...basePath)
|
||||
await this.registerVendorAssets(event.packageConfig.name, assetPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if ( this.config.get('server.builtIns.assets.enabled', true) ) {
|
||||
const prefix = this.config.get('server.builtIns.assets.prefix', '/assets')
|
||||
this.logging.debug(`Registering built-in assets server with prefix: ${prefix}`)
|
||||
await this.registerRoutes(() => {
|
||||
Route.group(prefix, () => {
|
||||
Route.get('/**', staticServer({
|
||||
directoryListing: false,
|
||||
basePath: ['resources', 'assets'],
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,9 @@ export abstract class ViewEngine extends AppClass {
|
||||
config: (key: string, fallback?: any) => this.config.get(key, fallback),
|
||||
asset: (...parts: string[]) => this.routing.getAssetPath(...parts).toRemote,
|
||||
vendor: (namespace: string, ...parts: string[]) => this.routing.getVendorPath(namespace, ...parts).toRemote,
|
||||
named: (name: string) => this.routing.getNamedPath(name).toRemote,
|
||||
route: (...parts: string[]) => this.routing.getAppUrl().concat(...parts).toRemote,
|
||||
hasRoute: (name: string) => this.routing.hasNamedRoute(name),
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user