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 './Authentication'
|
||||||
|
|
||||||
export * from './config'
|
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 {error} from '../../http/response/ErrorResponseFactory'
|
||||||
import {NotAuthorizedError} from '../NotAuthorizedError'
|
import {NotAuthorizedError} from '../NotAuthorizedError'
|
||||||
import {HTTPStatus} from '../../util'
|
import {HTTPStatus} from '../../util'
|
||||||
|
import {redirect} from '../../http/response/RedirectResponseFactory'
|
||||||
|
import {Routing} from '../../service/Routing'
|
||||||
|
import {Session} from '../../http/session/Session'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthRequiredMiddleware extends Middleware {
|
export class AuthRequiredMiddleware extends Middleware {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly security!: SecurityContext
|
protected readonly security!: SecurityContext
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly routing!: Routing
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly session!: Session
|
||||||
|
|
||||||
async apply(): Promise<ResponseObject> {
|
async apply(): Promise<ResponseObject> {
|
||||||
if ( !this.security.hasUser() ) {
|
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 {error} from '../../http/response/ErrorResponseFactory'
|
||||||
import {NotAuthorizedError} from '../NotAuthorizedError'
|
import {NotAuthorizedError} from '../NotAuthorizedError'
|
||||||
import {HTTPStatus} from '../../util'
|
import {HTTPStatus} from '../../util'
|
||||||
|
import {Routing} from '../../service/Routing'
|
||||||
|
import {redirect} from '../../http/response/RedirectResponseFactory'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GuestRequiredMiddleware extends Middleware {
|
export class GuestRequiredMiddleware extends Middleware {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly security!: SecurityContext
|
protected readonly security!: SecurityContext
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly routing!: Routing
|
||||||
|
|
||||||
async apply(): Promise<ResponseObject> {
|
async apply(): Promise<ResponseObject> {
|
||||||
if ( this.security.hasUser() ) {
|
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 {AppClass} from '../../lifecycle/AppClass'
|
||||||
import {Request} from '../lifecycle/Request'
|
import {Request} from '../lifecycle/Request'
|
||||||
import {error} from '../response/ErrorResponseFactory'
|
import {error} from '../response/ErrorResponseFactory'
|
||||||
|
import {HTTPError} from '../HTTPError'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for fluently registering kernel modules into the kernel.
|
* Interface for fluently registering kernel modules into the kernel.
|
||||||
@ -105,7 +106,8 @@ export class HTTPKernel extends AppClass {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logging.error(e)
|
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)
|
.write(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +138,6 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
|||||||
})
|
})
|
||||||
|
|
||||||
busboy.on('finish', () => {
|
busboy.on('finish', () => {
|
||||||
this.logging.debug(`Parsed body input: ${JSON.stringify(request.parsedInput)}`)
|
|
||||||
res()
|
res()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
|
|||||||
public async apply(request: Request): Promise<Request> {
|
public async apply(request: Request): Promise<Request> {
|
||||||
if ( !this.config.get('server.poweredBy.hide', false) ) {
|
if ( !this.config.get('server.poweredBy.hide', false) ) {
|
||||||
request.response.setHeader('X-Powered-By', this.config.get('server.poweredBy.header', 'Extollo'))
|
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
|
return request
|
||||||
|
@ -229,6 +229,9 @@ export class Response {
|
|||||||
this.setHeader('Content-Length', String(this.body?.length ?? 0))
|
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 ?? '')
|
await this.write(this.body ?? '')
|
||||||
this.end()
|
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.
|
* Helper function to create a new TemporaryRedirectResponseFactory to the given destination.
|
||||||
* @param destination
|
* @param destination
|
||||||
*/
|
*/
|
||||||
export function redirect(destination: string): TemporaryRedirectResponseFactory {
|
export function temporary(destination: string): TemporaryRedirectResponseFactory {
|
||||||
return new TemporaryRedirectResponseFactory(destination)
|
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. */
|
/** Pre-compiled route handler for the main route handler for this route. */
|
||||||
protected compiledPostflight?: ResolvedRouteHandler[]
|
protected compiledPostflight?: ResolvedRouteHandler[]
|
||||||
|
|
||||||
|
/** Programmatic aliases of this route. */
|
||||||
|
public aliases: string[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
/** The HTTP method(s) that this route listens on. */
|
/** The HTTP method(s) that this route listens on. */
|
||||||
protected method: HTTPMethod | HTTPMethod[],
|
protected method: HTTPMethod | HTTPMethod[],
|
||||||
@ -228,6 +231,15 @@ export class Route extends AppClass {
|
|||||||
super()
|
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.
|
* Get the string-form of the route.
|
||||||
*/
|
*/
|
||||||
|
@ -5,7 +5,7 @@ import {Collection, HTTPStatus, UniversalPath, universalPath} from '../../util'
|
|||||||
import {Application} from '../../lifecycle/Application'
|
import {Application} from '../../lifecycle/Application'
|
||||||
import {HTTPError} from '../HTTPError'
|
import {HTTPError} from '../HTTPError'
|
||||||
import {view, ViewResponseFactory} from '../response/ViewResponseFactory'
|
import {view, ViewResponseFactory} from '../response/ViewResponseFactory'
|
||||||
import {redirect} from '../response/TemporaryRedirectResponseFactory'
|
import {redirect} from '../response/RedirectResponseFactory'
|
||||||
import {file} from '../response/FileResponseFactory'
|
import {file} from '../response/FileResponseFactory'
|
||||||
import {RouteHandler} from '../routing/Route'
|
import {RouteHandler} from '../routing/Route'
|
||||||
|
|
||||||
@ -24,6 +24,9 @@ export interface StaticServerOptions {
|
|||||||
|
|
||||||
/** If specified, files with these extensions will not be served. */
|
/** If specified, files with these extensions will not be served. */
|
||||||
excludedExtensions?: string[]
|
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 the resolved path is a directory, send the directory listing response
|
||||||
if ( await filePath.isDirectory() ) {
|
if ( await filePath.isDirectory() ) {
|
||||||
|
if ( options.indexFile ) {
|
||||||
|
const indexFile = filePath.concat(options.indexFile)
|
||||||
|
if ( await indexFile.exists() ) {
|
||||||
|
return file(indexFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( !options.directoryListing ) {
|
if ( !options.directoryListing ) {
|
||||||
throw new StaticServerHTTPError(HTTPStatus.NOT_FOUND, 'File not found', {
|
throw new StaticServerHTTPError(HTTPStatus.NOT_FOUND, 'File not found', {
|
||||||
basePath: basePath.toString(),
|
basePath: basePath.toString(),
|
||||||
|
@ -93,4 +93,12 @@ export class MemorySession extends Session {
|
|||||||
|
|
||||||
this.data[key] = value
|
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. */
|
/** Set a value in the session by key. */
|
||||||
public abstract set(key: string, value: unknown): void
|
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/ResponseFactory'
|
||||||
export * from './http/response/StringResponseFactory'
|
export * from './http/response/StringResponseFactory'
|
||||||
export * from './http/response/TemporaryRedirectResponseFactory'
|
export * from './http/response/TemporaryRedirectResponseFactory'
|
||||||
|
export * from './http/response/RedirectResponseFactory'
|
||||||
export * from './http/response/ViewResponseFactory'
|
export * from './http/response/ViewResponseFactory'
|
||||||
export * from './http/response/FileResponseFactory'
|
export * from './http/response/FileResponseFactory'
|
||||||
|
export * from './http/response/RouteResponseFactory'
|
||||||
|
|
||||||
export * from './http/routing/ActivatedRoute'
|
export * from './http/routing/ActivatedRoute'
|
||||||
export * from './http/routing/Route'
|
export * from './http/routing/Route'
|
||||||
|
@ -68,6 +68,7 @@ export class ORMSession extends Session {
|
|||||||
if ( !this.data ) {
|
if ( !this.data ) {
|
||||||
throw new SessionNotLoadedError()
|
throw new SessionNotLoadedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.data[key] ?? fallback
|
return this.data[key] ?? fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +76,15 @@ export class ORMSession extends Session {
|
|||||||
if ( !this.data ) {
|
if ( !this.data ) {
|
||||||
throw new SessionNotLoadedError()
|
throw new SessionNotLoadedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.data[key] = value
|
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')}
|
title Login | #{config('app.name', 'Extollo')}
|
||||||
|
|
||||||
block heading
|
block heading
|
||||||
| Login to Continue
|
| Login to continue
|
||||||
|
|
||||||
block form
|
block form
|
||||||
.form-label-group
|
.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
|
label(for='inputUsername') Username
|
||||||
|
|
||||||
.form-label-group
|
.form-label-group
|
||||||
input#inputPassword.form-control(type='password' name='password' required placeholder='Password')
|
input#inputPassword.form-control(type='password' name='password' required placeholder='Password')
|
||||||
label(for='inputPassword') 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
|
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
|
.text-center
|
||||||
span.small Need an account?
|
span.small Need an account?
|
||||||
a(href='./register') Register here.
|
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 head
|
||||||
|
|
||||||
block styles
|
block styles
|
||||||
link(rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css')
|
link(rel='stylesheet' href=vendor('@extollo/lib', 'lib/bootstrap.min.css'))
|
||||||
link(rel='stylesheet' href=vendor('@extollo', 'auth/theme.css'))
|
link(rel='stylesheet' href=vendor('@extollo/lib', 'auth/theme.css'))
|
||||||
body
|
body
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row.no-gutter
|
.row.no-gutter
|
||||||
.d-none.d-md-flex.col-md-6.col-lg-8.bg-image
|
.col-md-12.col-lg-12
|
||||||
.col-md-6.col-lg-4
|
|
||||||
.login.d-flex.align-items-center.py-5
|
.login.d-flex.align-items-center.py-5
|
||||||
.container
|
.container
|
||||||
.row
|
.row
|
||||||
.col-md-9.col-lg-8.mx-auto
|
.col-md-9.col-lg-6.mx-auto
|
||||||
block content
|
block content
|
||||||
|
|
||||||
block scripts
|
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 {Inject, Singleton} from '../di'
|
||||||
import {HTTPStatus, universalPath, UniversalPath, withTimeout} from '../util'
|
import {HTTPStatus, withTimeout} from '../util'
|
||||||
import {Unit} from '../lifecycle/Unit'
|
import {Unit} from '../lifecycle/Unit'
|
||||||
import {createServer, IncomingMessage, RequestListener, Server, ServerResponse} from 'http'
|
import {createServer, IncomingMessage, RequestListener, Server, ServerResponse} from 'http'
|
||||||
import {Logging} from './Logging'
|
import {Logging} from './Logging'
|
||||||
@ -18,10 +18,7 @@ import {ParseIncomingBodyHTTPModule} from '../http/kernel/module/ParseIncomingBo
|
|||||||
import {Config} from './Config'
|
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 {Route} from '../http/routing/Route'
|
|
||||||
import {staticServer} from '../http/servers/static'
|
|
||||||
import {EventBus} from '../event/EventBus'
|
import {EventBus} from '../event/EventBus'
|
||||||
import {PackageDiscovered} from '../support/PackageDiscovered'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@ -47,30 +44,6 @@ export class HTTPServer extends Unit {
|
|||||||
/** The underlying native Node.js server. */
|
/** The underlying native Node.js server. */
|
||||||
protected server?: 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> {
|
public async up(): Promise<void> {
|
||||||
const port = this.config.get('server.port', 8000)
|
const port = this.config.get('server.port', 8000)
|
||||||
|
|
||||||
@ -86,8 +59,6 @@ export class HTTPServer extends Unit {
|
|||||||
ParseIncomingBodyHTTPModule.register(this.kernel)
|
ParseIncomingBodyHTTPModule.register(this.kernel)
|
||||||
InjectRequestEventBusHTTPModule.register(this.kernel)
|
InjectRequestEventBusHTTPModule.register(this.kernel)
|
||||||
|
|
||||||
await this.registerBuiltIns()
|
|
||||||
|
|
||||||
await new Promise<void>(res => {
|
await new Promise<void>(res => {
|
||||||
this.server = createServer(this.handler)
|
this.server = createServer(this.handler)
|
||||||
|
|
||||||
@ -148,34 +119,4 @@ export class HTTPServer extends Unit {
|
|||||||
await extolloReq.response.send()
|
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 {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 {Unit, UnitStatus} from '../lifecycle/Unit'
|
||||||
import {Logging} from './Logging'
|
import {Logging} from './Logging'
|
||||||
import {Route} from '../http/routing/Route'
|
import {Route} from '../http/routing/Route'
|
||||||
@ -10,6 +10,7 @@ import {lib} from '../lib'
|
|||||||
import {Config} from './Config'
|
import {Config} from './Config'
|
||||||
import {EventBus} from '../event/EventBus'
|
import {EventBus} from '../event/EventBus'
|
||||||
import {PackageDiscovered} from '../support/PackageDiscovered'
|
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.
|
* 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 import(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.registerBuiltIns()
|
||||||
|
|
||||||
this.logging.info('Compiling routes...')
|
this.logging.info('Compiling routes...')
|
||||||
this.compiledRoutes = new Collection<Route>(await Route.compile())
|
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'))
|
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 {
|
public getAppUrl(): UniversalPath {
|
||||||
const rawHost = String(this.config.get('server.url', 'http://localhost')).toLowerCase()
|
const rawHost = String(this.config.get('server.url', 'http://localhost')).toLowerCase()
|
||||||
const isSSL = rawHost.startsWith('https://')
|
const isSSL = rawHost.startsWith('https://')
|
||||||
@ -166,4 +189,58 @@ export class Routing extends Unit {
|
|||||||
.tap<UniversalPath>(host => universalPath(host))
|
.tap<UniversalPath>(host => universalPath(host))
|
||||||
.get()
|
.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),
|
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,
|
||||||
vendor: (namespace: string, ...parts: string[]) => this.routing.getVendorPath(namespace, ...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