Add model serializer and coreid login provider
This commit is contained in:
parent
0774deea91
commit
70d67c2730
@ -9,6 +9,10 @@ import {GuestRequiredMiddleware} from './middleware/GuestRequiredMiddleware'
|
|||||||
import {SessionAuthMiddleware} from './middleware/SessionAuthMiddleware'
|
import {SessionAuthMiddleware} from './middleware/SessionAuthMiddleware'
|
||||||
import {ViewEngine} from '../views/ViewEngine'
|
import {ViewEngine} from '../views/ViewEngine'
|
||||||
import {SecurityContext} from './context/SecurityContext'
|
import {SecurityContext} from './context/SecurityContext'
|
||||||
|
import {LoginProvider, LoginProviderConfig} from './provider/LoginProvider'
|
||||||
|
import {Config} from '../service/Config'
|
||||||
|
import {ErrorWithContext, hasOwnProperty} from '../util'
|
||||||
|
import {Route} from '../http/routing/Route'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Authentication extends Unit {
|
export class Authentication extends Unit {
|
||||||
@ -18,6 +22,11 @@ export class Authentication extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly middleware!: Middlewares
|
protected readonly middleware!: Middlewares
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly config!: Config
|
||||||
|
|
||||||
|
protected providers: {[name: string]: LoginProvider<LoginProviderConfig>} = {}
|
||||||
|
|
||||||
async up(): Promise<void> {
|
async up(): Promise<void> {
|
||||||
this.middleware.registerNamespace('@auth', this.getMiddlewareResolver())
|
this.middleware.registerNamespace('@auth', this.getMiddlewareResolver())
|
||||||
|
|
||||||
@ -27,6 +36,33 @@ export class Authentication extends Unit {
|
|||||||
return () => req?.make<SecurityContext>(SecurityContext)?.getUser()
|
return () => req?.make<SecurityContext>(SecurityContext)?.getUser()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const config = this.config.get('auth.providers', {})
|
||||||
|
const middleware = this.config.get('auth.middleware', SessionAuthMiddleware)
|
||||||
|
|
||||||
|
if ( !(middleware?.prototype instanceof Middleware) ) {
|
||||||
|
throw new ErrorWithContext('Auth middleware must extend Middleware base class', {
|
||||||
|
providedValue: middleware,
|
||||||
|
configKey: 'auth.middleware',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const name in config ) {
|
||||||
|
if ( !hasOwnProperty(config, name) ) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.providers[name] ) {
|
||||||
|
this.logging.warn(`Registering duplicate authentication provider: ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logging.verbose(`Registered authentication provider: ${name}`)
|
||||||
|
this.providers[name] = this.make(config[name].driver, name, config[name].config)
|
||||||
|
|
||||||
|
Route.group(`/auth/${name}`, () => {
|
||||||
|
this.providers[name].routes()
|
||||||
|
}).pre(request => request.make<Middleware>(middleware, request).apply())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getMiddlewareResolver(): CanonicalResolver<StaticInstantiable<Middleware>> {
|
protected getMiddlewareResolver(): CanonicalResolver<StaticInstantiable<Middleware>> {
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import {Instantiable, isInstantiable} from '../di'
|
import {Instantiable, isInstantiable} from '../di'
|
||||||
import {AuthenticatableRepository} from './types'
|
import {AuthenticatableRepository} from './types'
|
||||||
import {hasOwnProperty} from '../util'
|
import {hasOwnProperty} from '../util'
|
||||||
import {LoginProviderConfig} from './provider/LoginProvider'
|
import {LoginProvider, LoginProviderConfig} from './provider/LoginProvider'
|
||||||
|
import {Middleware} from '../http/routing/Middleware'
|
||||||
|
|
||||||
export interface AuthenticationConfig {
|
export interface AuthenticationConfig {
|
||||||
storage: Instantiable<AuthenticatableRepository>,
|
storage: Instantiable<AuthenticatableRepository>,
|
||||||
|
middleware?: Instantiable<Middleware>,
|
||||||
providers?: {
|
providers?: {
|
||||||
[key: string]: LoginProviderConfig
|
[key: string]: {
|
||||||
|
driver: Instantiable<LoginProvider<LoginProviderConfig>>,
|
||||||
|
config: LoginProviderConfig,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,7 +20,7 @@ export function isAuthenticationConfig(what: unknown): what is AuthenticationCon
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !hasOwnProperty(what, 'storage') || !hasOwnProperty(what, 'sources') ) {
|
if ( !hasOwnProperty(what, 'storage') || !hasOwnProperty(what, 'providers') ) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,17 +28,21 @@ export function isAuthenticationConfig(what: unknown): what is AuthenticationCon
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( typeof what.sources !== 'object' ) {
|
if ( typeof what.providers !== 'object' ) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for ( const key in what.sources ) {
|
for ( const key in what.providers ) {
|
||||||
if ( !hasOwnProperty(what.sources, key) ) {
|
if ( !hasOwnProperty(what.providers, key) ) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = what.sources[key]
|
const source = what.providers[key]
|
||||||
if ( !isInstantiable(source) || !(source.prototype instanceof AuthenticatableRepository) ) {
|
if ( typeof source !== 'object' || source === null || !hasOwnProperty(source, 'driver') ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !isInstantiable(source.driver) || !(source.driver.prototype instanceof LoginProvider) ) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,17 @@ export * from './event/UserAuthenticatedEvent'
|
|||||||
export * from './event/UserAuthenticationResumedEvent'
|
export * from './event/UserAuthenticationResumedEvent'
|
||||||
export * from './event/UserFlushedEvent'
|
export * from './event/UserFlushedEvent'
|
||||||
|
|
||||||
|
export * from './middleware/AuthRequiredMiddleware'
|
||||||
|
export * from './middleware/GuestRequiredMiddleware'
|
||||||
|
export * from './middleware/SessionAuthMiddleware'
|
||||||
|
|
||||||
|
export * from './provider/basic/BasicLoginAttempt'
|
||||||
|
export * from './provider/basic/BasicLoginProvider'
|
||||||
|
export * from './provider/basic/BasicRegistrationAttempt'
|
||||||
|
|
||||||
|
export * from './provider/oauth/OAuth2LoginProvider'
|
||||||
|
export * from './provider/oauth/CoreIDLoginProvider'
|
||||||
|
|
||||||
export * from './serial/AuthenticationEventSerializer'
|
export * from './serial/AuthenticationEventSerializer'
|
||||||
|
|
||||||
export * from './repository/orm/ORMUser'
|
export * from './repository/orm/ORMUser'
|
||||||
|
@ -5,6 +5,7 @@ import {AuthRequiredMiddleware} from '../middleware/AuthRequiredMiddleware'
|
|||||||
import {Inject, Injectable} from '../../di'
|
import {Inject, Injectable} from '../../di'
|
||||||
import {SecurityContext} from '../context/SecurityContext'
|
import {SecurityContext} from '../context/SecurityContext'
|
||||||
import {redirect} from '../../http/response/RedirectResponseFactory'
|
import {redirect} from '../../http/response/RedirectResponseFactory'
|
||||||
|
import {RequestLocalStorage} from '../../http/RequestLocalStorage'
|
||||||
|
|
||||||
export interface LoginProviderConfig {
|
export interface LoginProviderConfig {
|
||||||
default: boolean,
|
default: boolean,
|
||||||
@ -17,7 +18,11 @@ export interface LoginProviderConfig {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export abstract class LoginProvider<TConfig extends LoginProviderConfig> {
|
export abstract class LoginProvider<TConfig extends LoginProviderConfig> {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly security!: SecurityContext
|
protected readonly request!: RequestLocalStorage
|
||||||
|
|
||||||
|
protected get security(): SecurityContext {
|
||||||
|
return this.request.get().make(SecurityContext)
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected name: string,
|
protected name: string,
|
||||||
|
100
src/auth/provider/oauth/CoreIDLoginProvider.ts
Normal file
100
src/auth/provider/oauth/CoreIDLoginProvider.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/* eslint camelcase: 0 */
|
||||||
|
import {OAuth2LoginProvider, OAuth2LoginProviderConfig} from './OAuth2LoginProvider'
|
||||||
|
import {Authenticatable} from '../../types'
|
||||||
|
import {Request} from '../../../http/lifecycle/Request'
|
||||||
|
import {ErrorWithContext, uuid4, fetch} from '../../../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth2LoginProvider implementation that authenticates users against a
|
||||||
|
* Starship CoreID server.
|
||||||
|
*/
|
||||||
|
export class CoreIDLoginProvider extends OAuth2LoginProvider<OAuth2LoginProviderConfig> {
|
||||||
|
protected async callback(request: Request): Promise<Authenticatable> {
|
||||||
|
// Get authentication_code from the request
|
||||||
|
const code = String(request.input('code') || '')
|
||||||
|
if ( !code ) {
|
||||||
|
throw new ErrorWithContext('Unable to authenticate user: missing login code', {
|
||||||
|
input: request.input(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get OAuth2 token from CoreID
|
||||||
|
const token = await this.getToken(code)
|
||||||
|
|
||||||
|
// Get user from endpoint
|
||||||
|
const userData = await this.getUserData(token)
|
||||||
|
|
||||||
|
// Return authenticatable instance
|
||||||
|
const existing = await this.security.repository.getByIdentifier(userData.uid)
|
||||||
|
if ( existing ) {
|
||||||
|
this.updateUser(existing, userData)
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.security.repository.createFromCredentials(userData.uid, uuid4())
|
||||||
|
this.updateUser(user, userData)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Given an access token, look up the associated user's information. */
|
||||||
|
protected async getUserData(token: string): Promise<any> {
|
||||||
|
const userResponse = await fetch(
|
||||||
|
this.config.userUrl,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const userData: any = await userResponse.json()
|
||||||
|
if ( !userData?.data?.uid ) {
|
||||||
|
throw new ErrorWithContext('Unable to extract user from response', {
|
||||||
|
userData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return userData.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Given a login code, redeem it for an access token. */
|
||||||
|
protected async getToken(code: string): Promise<string> {
|
||||||
|
const body: string[] = [
|
||||||
|
'code=' + encodeURIComponent(code),
|
||||||
|
'client_id=' + encodeURIComponent(this.config.clientId),
|
||||||
|
'client_secret=' + encodeURIComponent(this.config.clientSecret),
|
||||||
|
'grant_type=authorization_code',
|
||||||
|
]
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
this.config.tokenUrl,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: body.join('&'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
const token = (data as any).access_token
|
||||||
|
if ( !token ) {
|
||||||
|
throw new ErrorWithContext('Unable to obtain access token from response', {
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update values on the Authenticatable from user data. */
|
||||||
|
protected updateUser(user: any, data: any): void {
|
||||||
|
user.firstName = data.first_name
|
||||||
|
user.lastName = data.last_name
|
||||||
|
user.email = data.email
|
||||||
|
user.tagline = data.tagline
|
||||||
|
user.photoUrl = data.profile_photo
|
||||||
|
}
|
||||||
|
}
|
98
src/auth/provider/oauth/OAuth2LoginProvider.ts
Normal file
98
src/auth/provider/oauth/OAuth2LoginProvider.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import {LoginProvider, LoginProviderConfig} from '../LoginProvider'
|
||||||
|
import {ResponseObject, Route} from '../../../http/routing/Route'
|
||||||
|
import {Inject, Injectable} from '../../../di'
|
||||||
|
import {Routing} from '../../../service/Routing'
|
||||||
|
import {GuestRequiredMiddleware} from '../../middleware/GuestRequiredMiddleware'
|
||||||
|
import {redirect} from '../../../http/response/RedirectResponseFactory'
|
||||||
|
import {view} from '../../../http/response/ViewResponseFactory'
|
||||||
|
import {Request} from '../../../http/lifecycle/Request'
|
||||||
|
import {Awaitable} from '../../../util'
|
||||||
|
import {Authenticatable} from '../../types'
|
||||||
|
|
||||||
|
export interface OAuth2LoginProviderConfig extends LoginProviderConfig {
|
||||||
|
displayName: string,
|
||||||
|
clientId: string|number
|
||||||
|
clientSecret: string
|
||||||
|
loginUrl: string
|
||||||
|
loginMessage?: string
|
||||||
|
logoutUrl?: string
|
||||||
|
tokenUrl: string,
|
||||||
|
userUrl: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LoginProvider implementation for OAuth2-based logins.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export abstract class OAuth2LoginProvider<TConfig extends OAuth2LoginProviderConfig> extends LoginProvider<TConfig> {
|
||||||
|
@Inject()
|
||||||
|
protected readonly routing!: Routing
|
||||||
|
|
||||||
|
public routes(): void {
|
||||||
|
super.routes()
|
||||||
|
|
||||||
|
Route.any('redirect')
|
||||||
|
.alias(`@auth:${this.name}:redirect`)
|
||||||
|
.pre(GuestRequiredMiddleware)
|
||||||
|
.handledBy(() => redirect(this.getLoginUrl()))
|
||||||
|
|
||||||
|
Route.any('callback')
|
||||||
|
.alias(`@auth:${this.name}:callback`)
|
||||||
|
.pre(GuestRequiredMiddleware)
|
||||||
|
.passingRequest()
|
||||||
|
.handledBy(this.handleCallback.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleCallback(request: Request): Promise<ResponseObject> {
|
||||||
|
const user = await this.callback(request)
|
||||||
|
if ( user ) {
|
||||||
|
await this.security.authenticate(user)
|
||||||
|
return this.redirectToIntendedRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(this.routing.getNamedPath(`@auth:${this.name}:login`).toRemote)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After redirecting back from the OAuth2 server, look up the user information.
|
||||||
|
* @param request
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected abstract callback(request: Request): Awaitable<Authenticatable>
|
||||||
|
|
||||||
|
public login(): ResponseObject {
|
||||||
|
const buttonUrl = this.routing
|
||||||
|
.getNamedPath(`@auth:${this.name}:redirect`)
|
||||||
|
.toRemote
|
||||||
|
|
||||||
|
return view('@extollo:auth:message', {
|
||||||
|
message: this.config.loginMessage ?? `Sign-in with ${this.config.displayName} to continue`,
|
||||||
|
buttonText: 'Sign-in',
|
||||||
|
buttonUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(): Promise<ResponseObject> {
|
||||||
|
await this.security.flush()
|
||||||
|
|
||||||
|
if ( this.config.logoutUrl ) {
|
||||||
|
return redirect(this.config.logoutUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('@extollo:auth:message', {
|
||||||
|
message: 'You have been signed-out',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL where the user should be redirected to sign-in.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected getLoginUrl(): string {
|
||||||
|
const callbackRoute = this.routing.getNamedPath(`@auth:${this.name}:callback`)
|
||||||
|
|
||||||
|
return this.config.loginUrl
|
||||||
|
.replace(/%c/g, String(this.config.clientId))
|
||||||
|
.replace(/%r/g, callbackRoute.toRemote)
|
||||||
|
}
|
||||||
|
}
|
@ -23,12 +23,15 @@ export class RoutesDirective extends Directive {
|
|||||||
const maxHandlerLength = compiled.mapCall('getHandlerDisplay')
|
const maxHandlerLength = compiled.mapCall('getHandlerDisplay')
|
||||||
.whereDefined()
|
.whereDefined()
|
||||||
.max('length')
|
.max('length')
|
||||||
|
const maxNameLength = compiled.mapCall('getAlias')
|
||||||
|
.whereDefined()
|
||||||
|
.max('length')
|
||||||
|
|
||||||
const rows = compiled.map(route => [String(route), route.getHandlerDisplay()])
|
const rows = compiled.map(route => [String(route), route.getHandlerDisplay(), route.getAlias() || ''])
|
||||||
|
|
||||||
const table = new Table({
|
const table = new Table({
|
||||||
head: ['Route', 'Handler'],
|
head: ['Route', 'Handler', 'Name'],
|
||||||
colWidths: [maxRouteLength + 2, maxHandlerLength + 2],
|
colWidths: [maxRouteLength + 2, maxHandlerLength + 2, maxNameLength + 2],
|
||||||
})
|
})
|
||||||
|
|
||||||
table.push(...rows.toArray())
|
table.push(...rows.toArray())
|
||||||
|
@ -398,6 +398,10 @@ export class Route<TReturn extends ResponseObject, THandlerParams extends unknow
|
|||||||
return this.aliases.includes(name)
|
return this.aliases.includes(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAlias(): Maybe<string> {
|
||||||
|
return this.aliases.first()
|
||||||
|
}
|
||||||
|
|
||||||
isHandled(): this is HandledRoute<TReturn, THandlerParams> {
|
isHandled(): this is HandledRoute<TReturn, THandlerParams> {
|
||||||
return Boolean(this.handler)
|
return Boolean(this.handler)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import {BaseSerializer} from '../../../support/bus'
|
import {BaseSerializer, ObjectSerializer} from '../../../support/bus'
|
||||||
import {QueryExecutedEvent} from './QueryExecutedEvent'
|
import {QueryExecutedEvent} from './QueryExecutedEvent'
|
||||||
import {Awaitable, JSONState} from '../../../util'
|
import {Awaitable, JSONState} from '../../../util'
|
||||||
import {Container, Inject, Injectable} from '../../../di'
|
import {Container, Inject, Injectable} from '../../../di'
|
||||||
import {DatabaseService} from '../../DatabaseService'
|
import {DatabaseService} from '../../DatabaseService'
|
||||||
import {ObjectSerializer} from '../../../support/bus/serial/decorators'
|
|
||||||
|
|
||||||
export interface QueryExecutedEventSerialPayload extends JSONState {
|
export interface QueryExecutedEventSerialPayload extends JSONState {
|
||||||
connectionName: string
|
connectionName: string
|
||||||
|
@ -7,6 +7,8 @@ export * from './builder/Builder'
|
|||||||
|
|
||||||
export * from './connection/Connection'
|
export * from './connection/Connection'
|
||||||
export * from './connection/PostgresConnection'
|
export * from './connection/PostgresConnection'
|
||||||
|
export * from './connection/event/QueryExecutedEvent'
|
||||||
|
export * from './connection/event/QueryExecutedEventSerializer'
|
||||||
|
|
||||||
export * from './dialect/SQLDialect'
|
export * from './dialect/SQLDialect'
|
||||||
export * from './dialect/PostgreSQLDialect'
|
export * from './dialect/PostgreSQLDialect'
|
||||||
@ -17,6 +19,7 @@ export * from './model/ModelBuilder'
|
|||||||
export * from './model/ModelResultIterable'
|
export * from './model/ModelResultIterable'
|
||||||
export * from './model/events'
|
export * from './model/events'
|
||||||
export * from './model/Model'
|
export * from './model/Model'
|
||||||
|
export * from './model/ModelSerializer'
|
||||||
|
|
||||||
export * from './model/relation/RelationBuilder'
|
export * from './model/relation/RelationBuilder'
|
||||||
export * from './model/relation/Relation'
|
export * from './model/relation/Relation'
|
||||||
|
68
src/orm/model/ModelSerializer.ts
Normal file
68
src/orm/model/ModelSerializer.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import {BaseSerializer, ObjectSerializer} from '../../support/bus'
|
||||||
|
import {Model} from './Model'
|
||||||
|
import {Awaitable, ErrorWithContext, JSONState, Maybe} from '../../util'
|
||||||
|
import {QueryRow} from '../types'
|
||||||
|
import {Inject, Injectable, isInstantiable} from '../../di'
|
||||||
|
import {Canon} from '../../service/Canon'
|
||||||
|
|
||||||
|
export interface ModelSerialPayload extends JSONState {
|
||||||
|
canonicalResolver: string,
|
||||||
|
primaryKey: any,
|
||||||
|
objectValues: QueryRow,
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectSerializer()
|
||||||
|
@Injectable()
|
||||||
|
export class ModelSerializer extends BaseSerializer<Model<any>, ModelSerialPayload> {
|
||||||
|
@Inject()
|
||||||
|
protected readonly canon!: Canon
|
||||||
|
|
||||||
|
protected async decodeSerial(serial: ModelSerialPayload): Promise<Model<any>> {
|
||||||
|
const ModelClass = this.canon.getFromFullyQualified(serial.canonicalResolver) as typeof Model
|
||||||
|
if ( !ModelClass || !(ModelClass.prototype instanceof Model) || !isInstantiable<Model<any>>(ModelClass) ) {
|
||||||
|
throw new ErrorWithContext('Cannot decode serialized model as canonical resolver is invalid', {
|
||||||
|
serial,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let inst: Maybe<Model<any>> = this.make<Model<any>>(ModelClass)
|
||||||
|
if ( serial.primaryKey ) {
|
||||||
|
inst = await ModelClass.query()
|
||||||
|
.whereKey(serial.primaryKey)
|
||||||
|
.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !inst ) {
|
||||||
|
throw new ErrorWithContext('Could not look up serialized model', {
|
||||||
|
serial,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await inst.assume(serial.objectValues)
|
||||||
|
return inst
|
||||||
|
}
|
||||||
|
|
||||||
|
protected encodeActual(actual: Model<any>): Awaitable<ModelSerialPayload> {
|
||||||
|
const ctor = actual.constructor as typeof Model
|
||||||
|
const canonicalResolver = ctor.getFullyQualifiedCanonicalResolver()
|
||||||
|
if ( !canonicalResolver ) {
|
||||||
|
throw new ErrorWithContext('Unable to serialize model: no Canonical resolver', {
|
||||||
|
actual,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canonicalResolver,
|
||||||
|
primaryKey: actual.key(),
|
||||||
|
objectValues: actual.toObject(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getName(): string {
|
||||||
|
return '@extollo/lib.ModelSerializer'
|
||||||
|
}
|
||||||
|
|
||||||
|
matchActual(some: Model<any>): boolean {
|
||||||
|
return some instanceof Model
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
extends ./theme
|
extends ../base
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
.offset(style='padding-top: 20vh')
|
||||||
if heading
|
if heading
|
||||||
h3.login-heading.mb-4 #{heading}
|
h3.login-heading #{heading}
|
||||||
|
else
|
||||||
|
h3.login-heading #{config('app.name') || ''}
|
||||||
|
|
||||||
if errors
|
if errors
|
||||||
each error in errors
|
each error in errors
|
||||||
@ -10,3 +13,6 @@ block content
|
|||||||
|
|
||||||
if message
|
if message
|
||||||
p #{message}
|
p #{message}
|
||||||
|
|
||||||
|
if buttonText && buttonUrl
|
||||||
|
a.button(href=buttonUrl) #{buttonText}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
html
|
|
||||||
head
|
|
||||||
meta(name='viewport' content='width=device-width initial-scale=1')
|
|
||||||
|
|
||||||
block head
|
|
||||||
|
|
||||||
block styles
|
|
||||||
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
|
|
||||||
.col-md-12.col-lg-12
|
|
||||||
.login.d-flex.align-items-center.py-5
|
|
||||||
.container
|
|
||||||
.row
|
|
||||||
.col-md-9.col-lg-6.mx-auto
|
|
||||||
block content
|
|
||||||
|
|
||||||
block scripts
|
|
||||||
script(src=vendor('@extollo/lib', 'lib/bootstrap.min.js'))
|
|
@ -3,14 +3,14 @@ import {BusSubscriber, Event, EventBus, EventHandler, EventHandlerReturn, EventH
|
|||||||
import {Awaitable, Collection, Pipeline, uuid4} from '../../util'
|
import {Awaitable, Collection, Pipeline, uuid4} from '../../util'
|
||||||
import {Logging} from '../../service/Logging'
|
import {Logging} from '../../service/Logging'
|
||||||
import {Bus, BusInternalSubscription} from './Bus'
|
import {Bus, BusInternalSubscription} from './Bus'
|
||||||
import {AppClass} from '../../lifecycle/AppClass'
|
|
||||||
import {getEventName} from './getEventName'
|
import {getEventName} from './getEventName'
|
||||||
|
import {CanonicalItemClass} from '../CanonicalReceiver'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Non-connectable event bus implementation. Can forward events to the main Bus instance.
|
* Non-connectable event bus implementation. Can forward events to the main Bus instance.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalBus<TEvent extends Event = Event> extends AppClass implements EventBus<TEvent> {
|
export class LocalBus<TEvent extends Event = Event> extends CanonicalItemClass implements EventBus<TEvent> {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly logging!: Logging
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user