Named routes & basic login framework

This commit is contained in:
2021-07-17 12:49:07 -05:00
parent e33d8dee8f
commit e86cf420df
26 changed files with 412 additions and 77 deletions

View File

@@ -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)
}

View File

@@ -138,7 +138,6 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
})
busboy.on('finish', () => {
this.logging.debug(`Parsed body input: ${JSON.stringify(request.parsedInput)}`)
res()
})

View File

@@ -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

View File

@@ -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()

View 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
}
}

View 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
}
}

View File

@@ -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)
}

View File

@@ -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.
*/

View File

@@ -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(),

View File

@@ -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]
}
}

View File

@@ -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
}