From caac5eefa881fb0355155ce5ff1c5505df725862 Mon Sep 17 00:00:00 2001 From: kokokuo Date: Mon, 1 Aug 2022 14:12:55 +0800 Subject: [PATCH 1/8] feat(serve): add auth middleware and authenticator - support "BaseAuthenticator" for user extending to customize authenticator. - add built-in "HttpBasicAuthenticator" for authenticate http basic token. - refactor type "KoaRouterConext" and "KoaNext". - create "AuthMiddleware" to handle authenticator through implemented authenticators. --- .../serve/src/containers/modules/extension.ts | 5 +- packages/serve/src/containers/types.ts | 3 + .../src/lib/auth/httpBasicAuthenticator.ts | 90 +++++++++++++++++++ packages/serve/src/lib/auth/index.ts | 5 ++ .../src/lib/middleware/auditLogMiddleware.ts | 5 +- .../src/lib/middleware/authMiddleware.ts | 66 ++++++++++++++ .../src/lib/middleware/corsMiddleware.ts | 5 +- packages/serve/src/lib/middleware/index.ts | 3 + .../src/lib/middleware/rateLimitMiddleware.ts | 5 +- .../src/lib/middleware/requestIdMiddleware.ts | 5 +- .../lib/middleware/response-format/helpers.ts | 6 +- .../middleware/response-format/middleware.ts | 5 +- .../strategy/cursorBasedStrategy.ts | 4 +- .../strategy/keysetBasedStrategy.ts | 4 +- .../strategy/offsetBasedStrategy.ts | 4 +- .../src/lib/pagination/strategy/strategy.ts | 4 +- .../lib/response-formatter/csvFormatter.ts | 4 +- .../lib/response-formatter/jsonFormatter.ts | 4 +- .../lib/route/route-component/baseRoute.ts | 16 ++-- .../lib/route/route-component/graphQLRoute.ts | 10 ++- .../route-component/paginationTransformer.ts | 6 +- .../route-component/requestTransformer.ts | 17 ++-- .../lib/route/route-component/restfulRoute.ts | 11 +-- packages/serve/src/models/appConfig.ts | 3 - packages/serve/src/models/context.ts | 7 ++ .../src/models/extensions/authenticator.ts | 34 +++++++ packages/serve/src/models/extensions/index.ts | 1 + .../models/extensions/responseFormatter.ts | 13 ++- .../src/models/extensions/routeMiddleware.ts | 7 +- packages/serve/src/models/index.ts | 5 +- .../{serveConfig.ts => serveOptions.ts} | 4 +- packages/serve/src/models/userAuthOptions.ts | 12 +++ packages/serve/test/app.spec.ts | 16 ++-- .../auditLogMiddleware.spec.ts | 10 +-- .../requestIdMiddleware.spec.ts | 18 ++-- .../formatResponseMiddleware.spec.ts | 15 ++-- .../response-format/helpers.spec.ts | 15 ++-- .../testModeMiddleware.ts | 4 +- .../serve/test/response-formatter/csv.spec.ts | 6 +- .../test/response-formatter/json.spec.ts | 6 +- .../paginationTransformer.spec.ts | 12 +-- .../requestTransformer.spec.ts | 14 +-- tsconfig.base.json | 4 +- 43 files changed, 350 insertions(+), 143 deletions(-) create mode 100644 packages/serve/src/lib/auth/httpBasicAuthenticator.ts create mode 100644 packages/serve/src/lib/auth/index.ts create mode 100644 packages/serve/src/lib/middleware/authMiddleware.ts delete mode 100644 packages/serve/src/models/appConfig.ts create mode 100644 packages/serve/src/models/context.ts create mode 100644 packages/serve/src/models/extensions/authenticator.ts rename packages/serve/src/models/{serveConfig.ts => serveOptions.ts} (66%) create mode 100644 packages/serve/src/models/userAuthOptions.ts diff --git a/packages/serve/src/containers/modules/extension.ts b/packages/serve/src/containers/modules/extension.ts index db5eaa64..6156336a 100644 --- a/packages/serve/src/containers/modules/extension.ts +++ b/packages/serve/src/containers/modules/extension.ts @@ -1,8 +1,9 @@ import { ExtensionLoader } from '@vulcan-sql/core'; import { AsyncContainerModule } from 'inversify'; -import { ServeConfig } from '../../models/serveConfig'; +import { ServeConfig } from '../../models/serveOptions'; import { BuiltInRouteMiddlewares } from '@vulcan-sql/serve/middleware'; import { BuiltInFormatters } from '@vulcan-sql/serve/response-formatter'; +import { BuiltInAuthenticators } from '../../lib/auth'; export const extensionModule = (options: ServeConfig) => new AsyncContainerModule(async (bind) => { @@ -13,6 +14,8 @@ export const extensionModule = (options: ServeConfig) => loader.loadInternalExtensionModule(BuiltInRouteMiddlewares); // formatter (single module) loader.loadInternalExtensionModule(BuiltInFormatters); + // authenticator (single module) + loader.loadInternalExtensionModule(BuiltInAuthenticators); loader.bindExtensions(bind); }); diff --git a/packages/serve/src/containers/types.ts b/packages/serve/src/containers/types.ts index 1cfddfe3..cb9a6916 100644 --- a/packages/serve/src/containers/types.ts +++ b/packages/serve/src/containers/types.ts @@ -5,10 +5,13 @@ export const TYPES = { PaginationTransformer: Symbol.for('PaginationTransformer'), Route: Symbol.for('Route'), RouteGenerator: Symbol.for('RouteGenerator'), + // Authenticator + UserAuthOptions: Symbol.for('UserAuthOptions'), // Application AppConfig: Symbol.for('AppConfig'), VulcanApplication: Symbol.for('VulcanApplication'), // Extensions Extension_RouteMiddleware: Symbol.for('Extension_RouteMiddleware'), + Extension_Authenticator: Symbol.for('Extension_Authenticator'), Extension_Formatter: Symbol.for('Extension_Formatter'), }; diff --git a/packages/serve/src/lib/auth/httpBasicAuthenticator.ts b/packages/serve/src/lib/auth/httpBasicAuthenticator.ts new file mode 100644 index 00000000..55648694 --- /dev/null +++ b/packages/serve/src/lib/auth/httpBasicAuthenticator.ts @@ -0,0 +1,90 @@ +import * as fs from 'fs'; +import { isEmpty } from 'lodash'; +import { + BaseAuthenticator, + KoaContext, + AuthResult, + UserAuthOptions, +} from '@vulcan-sql/serve/models'; +import { VulcanExtensionId, VulcanInternalExtension } from '@vulcan-sql/core'; + +@VulcanInternalExtension() +@VulcanExtensionId('basic') +export class HttpBasicAuthenticator extends BaseAuthenticator { + public async authenticate( + usersOptions: Array, + context: KoaContext + ) { + const auth = context.request.headers['authorization']?.toLowerCase(); + // if not exist basic method config, return failed. + if (isEmpty(usersOptions)) return { authenticated: false }; + if (!(auth && auth.startsWith(this.getExtensionId()!))) + return { authenticated: false }; + + // validate auth token + const token = auth.trim().split(' ')[1]; + + for (const userOptions of usersOptions) { + if (userOptions.auth['token']) { + const result = await this.verifyToken(token, userOptions); + if (result.authenticated) return result; + } + if (userOptions.auth['file']) { + const result = await this.verifyTokenInFile(token, userOptions); + if (result.authenticated) return result; + } + } + // if not found matched token, return failed + return { authenticated: false }; + } + + private async verifyToken(srcToken: string, userOptions: UserAuthOptions) { + let ansToken = ''; + const pattern = /^{{([\w]+|[ \w ]+)}}$/; + const matched = pattern.exec(userOptions.auth['token'] as string); + if (!matched) ansToken = ansToken || (userOptions.auth['token'] as string); + // matched[1] is env variable + if (matched) ansToken = ansToken || (process.env[matched[1]] as string); + if (srcToken === ansToken) + return { + authenticated: true, + user: { + name: userOptions.name, + method: this.getExtensionId()!, // method name + attr: userOptions.attr, + }, + } as AuthResult; + return { + authenticated: false, + }; + } + + private async verifyTokenInFile( + srcToken: string, + userOptions: UserAuthOptions + ) { + let ansToken = ''; + const filePath = userOptions.auth['filePath'] as string; + if (!fs.statSync(filePath).isFile()) return { authenticated: false }; + const stream = fs.createReadStream(filePath); + // read each line + stream.on('data', (chunk) => { + const content = chunk.toString(); + if (content.startsWith(`${userOptions.name}:`)) { + ansToken = content.split(':')[1]; + return; + } + }); + + if (srcToken !== ansToken) return { authenticated: false }; + // if matched return user data + return { + authenticated: true, + user: { + name: userOptions.name, + method: this.getExtensionId()!, // method name + attr: userOptions.attr, + }, + } as AuthResult; + } +} diff --git a/packages/serve/src/lib/auth/index.ts b/packages/serve/src/lib/auth/index.ts new file mode 100644 index 00000000..973b4ecc --- /dev/null +++ b/packages/serve/src/lib/auth/index.ts @@ -0,0 +1,5 @@ +export * from './httpBasicAuthenticator'; + +import { HttpBasicAuthenticator } from './httpBasicAuthenticator'; + +export const BuiltInAuthenticators = [HttpBasicAuthenticator]; diff --git a/packages/serve/src/lib/middleware/auditLogMiddleware.ts b/packages/serve/src/lib/middleware/auditLogMiddleware.ts index 72822129..931de4cf 100644 --- a/packages/serve/src/lib/middleware/auditLogMiddleware.ts +++ b/packages/serve/src/lib/middleware/auditLogMiddleware.ts @@ -3,8 +3,7 @@ import { LoggerOptions, VulcanInternalExtension, } from '@vulcan-sql/core'; -import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; +import { BuiltInMiddleware, KoaContext, Next } from '@vulcan-sql/serve/models'; @VulcanInternalExtension('audit-log') export class AuditLoggingMiddleware extends BuiltInMiddleware { @@ -13,7 +12,7 @@ export class AuditLoggingMiddleware extends BuiltInMiddleware { options: this.getOptions(), }); - public async handle(context: KoaRouterContext, next: KoaNext) { + public async handle(context: KoaContext, next: Next) { if (!this.enabled) return next(); const { path, request, params, response } = context; diff --git a/packages/serve/src/lib/middleware/authMiddleware.ts b/packages/serve/src/lib/middleware/authMiddleware.ts new file mode 100644 index 00000000..d68a0ddd --- /dev/null +++ b/packages/serve/src/lib/middleware/authMiddleware.ts @@ -0,0 +1,66 @@ +import { isEmpty } from 'lodash'; +import { inject, multiInject } from 'inversify'; +import { TYPES as CORE_TYPES, VulcanInternalExtension } from '@vulcan-sql/core'; +import { + Next, + KoaContext, + BuiltInMiddleware, + BaseAuthenticator, + UserAuthOptions, +} from '@vulcan-sql/serve/models'; +import { TYPES } from '@vulcan-sql/serve/containers'; + +export interface AuthOptions { + ['user-auth']?: Array; +} + +export type AuthenticatorMap = { + [name: string]: BaseAuthenticator; +}; + +/** The middleware used to check request auth information. + * It seek the 'auth' config to match data through built-in and customized authenticator by BaseAuthenticator + * */ +@VulcanInternalExtension('auth') +export class AuthMiddleware extends BuiltInMiddleware { + private authenticators: AuthenticatorMap; + constructor( + @inject(CORE_TYPES.ExtensionConfig) config: any, + @inject(CORE_TYPES.ExtensionName) name: string, + @multiInject(TYPES.Extension_Authenticator) + authenticators: BaseAuthenticator[] + ) { + super(config, name); + + this.authenticators = authenticators.reduce( + (prev, authenticator) => { + prev[authenticator.getExtensionId()!] = authenticator; + return prev; + }, + {} + ); + } + + public async handle(context: KoaContext, next: Next) { + // return to skip the middleware, if disabled + if (!this.enabled) return next(); + + const options = (this.getOptions() as AuthOptions) || {}; + if (isEmpty(options['user-auth'])) return next(); + + // authenticate each user by selected auth method in config + for (const name of Object.keys(this.authenticators)) { + const result = await this.authenticators[name].authenticate( + options['user-auth'] || [], + context + ); + if (!result.authenticated) continue; + + // set auth user information to context + context.state.user = result.user!; + await next(); + return; + } + throw new Error('authentication failed.'); + } +} diff --git a/packages/serve/src/lib/middleware/corsMiddleware.ts b/packages/serve/src/lib/middleware/corsMiddleware.ts index d0dc608a..a8b6e095 100644 --- a/packages/serve/src/lib/middleware/corsMiddleware.ts +++ b/packages/serve/src/lib/middleware/corsMiddleware.ts @@ -1,6 +1,5 @@ import * as cors from '@koa/cors'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; +import { BuiltInMiddleware, KoaContext, Next } from '@vulcan-sql/serve/models'; import { VulcanInternalExtension } from '@vulcan-sql/core'; export type CorsOptions = cors.Options; @@ -9,7 +8,7 @@ export type CorsOptions = cors.Options; export class CorsMiddleware extends BuiltInMiddleware { private koaCors = cors(this.getOptions()); - public async handle(context: KoaRouterContext, next: KoaNext) { + public async handle(context: KoaContext, next: Next) { if (!this.enabled) return next(); return this.koaCors(context, next); } diff --git a/packages/serve/src/lib/middleware/index.ts b/packages/serve/src/lib/middleware/index.ts index bab3afa3..b12ac1a3 100644 --- a/packages/serve/src/lib/middleware/index.ts +++ b/packages/serve/src/lib/middleware/index.ts @@ -2,9 +2,11 @@ export * from './corsMiddleware'; export * from './requestIdMiddleware'; export * from './auditLogMiddleware'; export * from './rateLimitMiddleware'; +export * from './authMiddleware'; export * from './response-format'; import { CorsMiddleware } from './corsMiddleware'; +import { AuthMiddleware } from './authMiddleware'; import { RateLimitMiddleware } from './rateLimitMiddleware'; import { RequestIdMiddleware } from './requestIdMiddleware'; import { AuditLoggingMiddleware } from './auditLogMiddleware'; @@ -15,6 +17,7 @@ import { ClassType, ExtensionBase } from '@vulcan-sql/core'; export const BuiltInRouteMiddlewares: ClassType[] = [ CorsMiddleware, RateLimitMiddleware, + AuthMiddleware, RequestIdMiddleware, AuditLoggingMiddleware, ResponseFormatMiddleware, diff --git a/packages/serve/src/lib/middleware/rateLimitMiddleware.ts b/packages/serve/src/lib/middleware/rateLimitMiddleware.ts index 9e34f616..fd52381d 100644 --- a/packages/serve/src/lib/middleware/rateLimitMiddleware.ts +++ b/packages/serve/src/lib/middleware/rateLimitMiddleware.ts @@ -1,6 +1,5 @@ import { RateLimit, RateLimitOptions } from 'koa2-ratelimit'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; +import { BuiltInMiddleware, KoaContext, Next } from '@vulcan-sql/serve/models'; import { VulcanInternalExtension } from '@vulcan-sql/core'; export { RateLimitOptions }; @@ -9,7 +8,7 @@ export { RateLimitOptions }; export class RateLimitMiddleware extends BuiltInMiddleware { private koaRateLimit = RateLimit.middleware(this.getOptions()); - public async handle(context: KoaRouterContext, next: KoaNext) { + public async handle(context: KoaContext, next: Next) { if (!this.enabled) return next(); return this.koaRateLimit(context, next); } diff --git a/packages/serve/src/lib/middleware/requestIdMiddleware.ts b/packages/serve/src/lib/middleware/requestIdMiddleware.ts index b9dcba31..2e04304f 100644 --- a/packages/serve/src/lib/middleware/requestIdMiddleware.ts +++ b/packages/serve/src/lib/middleware/requestIdMiddleware.ts @@ -4,8 +4,7 @@ import { asyncReqIdStorage, VulcanInternalExtension, } from '@vulcan-sql/core'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; -import { BuiltInMiddleware } from '@vulcan-sql/serve/models'; +import { BuiltInMiddleware, KoaContext, Next } from '@vulcan-sql/serve/models'; import { TYPES as CORE_TYPES } from '@vulcan-sql/core'; import { inject } from 'inversify'; @@ -32,7 +31,7 @@ export class RequestIdMiddleware extends BuiltInMiddleware { if (!this.options['name']) this.options['name'] = 'X-Request-ID'; if (!this.options['fieldIn']) this.options['fieldIn'] = FieldInType.HEADER; } - public async handle(context: KoaRouterContext, next: KoaNext) { + public async handle(context: KoaContext, next: Next) { if (!this.enabled) return next(); const { request } = context; diff --git a/packages/serve/src/lib/middleware/response-format/helpers.ts b/packages/serve/src/lib/middleware/response-format/helpers.ts index 39009b6d..2f6011b6 100644 --- a/packages/serve/src/lib/middleware/response-format/helpers.ts +++ b/packages/serve/src/lib/middleware/response-format/helpers.ts @@ -1,4 +1,4 @@ -import { KoaRouterContext } from '@vulcan-sql/serve/route'; +import { KoaContext } from '@vulcan-sql/serve/models'; import { BaseResponseFormatter } from '@vulcan-sql/serve/models'; export type ResponseFormatterMap = { @@ -12,7 +12,7 @@ export type ResponseFormatterMap = { * @returns boolean, is received */ export const isReceivedFormatRequest = ( - context: KoaRouterContext, + context: KoaContext, format: string ) => { if (context.request.path.endsWith(`.${format}`)) return true; @@ -32,7 +32,7 @@ export const checkUsableFormat = ({ supportedFormats, defaultFormat, }: { - context: KoaRouterContext; + context: KoaContext; formatters: ResponseFormatterMap; supportedFormats: string[]; defaultFormat: string; diff --git a/packages/serve/src/lib/middleware/response-format/middleware.ts b/packages/serve/src/lib/middleware/response-format/middleware.ts index f0f8fac0..6df0ac4a 100644 --- a/packages/serve/src/lib/middleware/response-format/middleware.ts +++ b/packages/serve/src/lib/middleware/response-format/middleware.ts @@ -1,8 +1,8 @@ +import { KoaContext, Next } from '@vulcan-sql/serve/models'; import { BaseResponseFormatter, BuiltInMiddleware, } from '@vulcan-sql/serve/models'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; import { checkUsableFormat, ResponseFormatterMap } from './helpers'; import { VulcanInternalExtension } from '@vulcan-sql/core'; import { TYPES as CORE_TYPES } from '@vulcan-sql/core'; @@ -40,8 +40,7 @@ export class ResponseFormatMiddleware extends BuiltInMiddleware format.toLowerCase()); this.defaultFormat = !options.default ? 'json' : options.default; } - - public async handle(context: KoaRouterContext, next: KoaNext) { + public async handle(context: KoaContext, next: Next) { // return to skip the middleware, if disabled if (!this.enabled) return next(); diff --git a/packages/serve/src/lib/pagination/strategy/cursorBasedStrategy.ts b/packages/serve/src/lib/pagination/strategy/cursorBasedStrategy.ts index 4c9a267e..90fe72a5 100644 --- a/packages/serve/src/lib/pagination/strategy/cursorBasedStrategy.ts +++ b/packages/serve/src/lib/pagination/strategy/cursorBasedStrategy.ts @@ -1,4 +1,4 @@ -import { RouterContext as KoaRouterContext } from 'koa-router'; +import { KoaContext } from '@vulcan-sql/serve/models'; import { normalizeStringValue, PaginationMode, @@ -7,7 +7,7 @@ import { import { PaginationStrategy } from './strategy'; export class CursorBasedStrategy extends PaginationStrategy { - public async transform(ctx: KoaRouterContext) { + public async transform(ctx: KoaContext) { const checkFelidInQueryString = ['limit', 'cursor'].every((field) => Object.keys(ctx.request.query).includes(field) ); diff --git a/packages/serve/src/lib/pagination/strategy/keysetBasedStrategy.ts b/packages/serve/src/lib/pagination/strategy/keysetBasedStrategy.ts index a39defca..1454208c 100644 --- a/packages/serve/src/lib/pagination/strategy/keysetBasedStrategy.ts +++ b/packages/serve/src/lib/pagination/strategy/keysetBasedStrategy.ts @@ -4,7 +4,7 @@ import { PaginationSchema, KeysetPagination, } from '@vulcan-sql/core'; -import { KoaRouterContext } from '@vulcan-sql/serve/route'; +import { KoaContext } from '@vulcan-sql/serve/models'; import { PaginationStrategy } from './strategy'; export class KeysetBasedStrategy extends PaginationStrategy { @@ -13,7 +13,7 @@ export class KeysetBasedStrategy extends PaginationStrategy { super(); this.pagination = pagination; } - public async transform(ctx: KoaRouterContext) { + public async transform(ctx: KoaContext) { if (!this.pagination.keyName) throw new Error( `The keyset pagination need to set "keyName" in schema for indicate what key need to do filter.` diff --git a/packages/serve/src/lib/pagination/strategy/offsetBasedStrategy.ts b/packages/serve/src/lib/pagination/strategy/offsetBasedStrategy.ts index 997f8a3e..00d28486 100644 --- a/packages/serve/src/lib/pagination/strategy/offsetBasedStrategy.ts +++ b/packages/serve/src/lib/pagination/strategy/offsetBasedStrategy.ts @@ -1,4 +1,4 @@ -import { RouterContext as KoaRouterContext } from 'koa-router'; +import { KoaContext } from '@vulcan-sql/serve/models'; import { normalizeStringValue, PaginationMode, @@ -7,7 +7,7 @@ import { import { PaginationStrategy } from './strategy'; export class OffsetBasedStrategy extends PaginationStrategy { - public async transform(ctx: KoaRouterContext) { + public async transform(ctx: KoaContext) { const checkFelidInQueryString = ['limit', 'offset'].every((field) => Object.keys(ctx.request.query).includes(field) ); diff --git a/packages/serve/src/lib/pagination/strategy/strategy.ts b/packages/serve/src/lib/pagination/strategy/strategy.ts index 0aa372b3..ff177553 100644 --- a/packages/serve/src/lib/pagination/strategy/strategy.ts +++ b/packages/serve/src/lib/pagination/strategy/strategy.ts @@ -1,5 +1,5 @@ -import { KoaRouterContext } from '@vulcan-sql/serve/route'; +import { KoaContext } from '@vulcan-sql/serve/models'; export abstract class PaginationStrategy { - public abstract transform(ctx: KoaRouterContext): Promise; + public abstract transform(ctx: KoaContext): Promise; } diff --git a/packages/serve/src/lib/response-formatter/csvFormatter.ts b/packages/serve/src/lib/response-formatter/csvFormatter.ts index a0870c75..0b6454ef 100644 --- a/packages/serve/src/lib/response-formatter/csvFormatter.ts +++ b/packages/serve/src/lib/response-formatter/csvFormatter.ts @@ -6,7 +6,7 @@ import { VulcanInternalExtension, } from '@vulcan-sql/core'; import { isArray, isObject, isUndefined } from 'lodash'; -import { KoaRouterContext } from '../route'; +import { KoaContext } from '@vulcan-sql/serve/models'; import { BaseResponseFormatter, toBuffer, @@ -109,7 +109,7 @@ export class CsvFormatter extends BaseResponseFormatter { public toResponse( stream: Stream.Readable | Stream.Transform, - ctx: KoaRouterContext + ctx: KoaContext ) { // get file name by url path. e.g: url = '/urls/orders', result = orders const size = ctx.url.split('/').length; diff --git a/packages/serve/src/lib/response-formatter/jsonFormatter.ts b/packages/serve/src/lib/response-formatter/jsonFormatter.ts index 3f0209ea..e1ec09e2 100644 --- a/packages/serve/src/lib/response-formatter/jsonFormatter.ts +++ b/packages/serve/src/lib/response-formatter/jsonFormatter.ts @@ -9,7 +9,7 @@ import { toBuffer, } from '../../models/extensions/responseFormatter'; import { isUndefined } from 'lodash'; -import { KoaRouterContext } from '../route'; +import { KoaContext } from '@vulcan-sql/serve/models'; const logger = getLogger({ scopeName: 'SERVE' }); @@ -76,7 +76,7 @@ export class JsonFormatter extends BaseResponseFormatter { public toResponse( stream: Stream.Readable | Stream.Transform, - ctx: KoaRouterContext + ctx: KoaContext ) { // set json stream to response in context ( data is json stream, no need to convert. ) ctx.response.body = stream; diff --git a/packages/serve/src/lib/route/route-component/baseRoute.ts b/packages/serve/src/lib/route/route-component/baseRoute.ts index 88b35752..0e08932a 100644 --- a/packages/serve/src/lib/route/route-component/baseRoute.ts +++ b/packages/serve/src/lib/route/route-component/baseRoute.ts @@ -1,5 +1,4 @@ -import { Next as KoaNext } from 'koa'; -import { RouterContext as KoaRouterContext } from 'koa-router'; +import { AuthUserInfo, KoaContext } from '@vulcan-sql/serve/models'; import { APISchema, TemplateEngine, @@ -10,8 +9,6 @@ import { IRequestValidator } from './requestValidator'; import { IRequestTransformer, RequestParameters } from './requestTransformer'; import { IPaginationTransformer } from './paginationTransformer'; -export { KoaRouterContext, KoaNext }; - export interface TransformedRequest { reqParams: RequestParameters; pagination?: Pagination; @@ -27,7 +24,7 @@ export interface RouteOptions { } export interface IRoute { - respond(ctx: KoaRouterContext): Promise; + respond(ctx: KoaContext): Promise; } export abstract class BaseRoute implements IRoute { @@ -53,13 +50,11 @@ export abstract class BaseRoute implements IRoute { this.templateEngine = templateEngine; } - public abstract respond(ctx: KoaRouterContext): Promise; + public abstract respond(ctx: KoaContext): Promise; - protected abstract prepare( - ctx: KoaRouterContext - ): Promise; + protected abstract prepare(ctx: KoaContext): Promise; - protected async handle(transformed: TransformedRequest) { + protected async handle(user: AuthUserInfo, transformed: TransformedRequest) { const { reqParams } = transformed; // could template name or template path, use for template engine const { templateSource } = this.apiSchema; @@ -67,6 +62,7 @@ export abstract class BaseRoute implements IRoute { const prepared = await this.dataSource.prepare(reqParams); const result = await this.templateEngine.execute(templateSource, { + ['user']: user, ['_prepared']: prepared, }); return result; diff --git a/packages/serve/src/lib/route/route-component/graphQLRoute.ts b/packages/serve/src/lib/route/route-component/graphQLRoute.ts index bf5698e8..5f65a1bb 100644 --- a/packages/serve/src/lib/route/route-component/graphQLRoute.ts +++ b/packages/serve/src/lib/route/route-component/graphQLRoute.ts @@ -1,4 +1,5 @@ -import { BaseRoute, KoaRouterContext, RouteOptions } from './baseRoute'; +import { BaseRoute, RouteOptions } from './baseRoute'; +import { KoaContext } from '@vulcan-sql/serve/models'; export class GraphQLRoute extends BaseRoute { public readonly operationName: string; @@ -13,15 +14,16 @@ export class GraphQLRoute extends BaseRoute { // TODO: generate graphql type by api schema } - public async respond(ctx: KoaRouterContext) { + public async respond(ctx: KoaContext) { const transformed = await this.prepare(ctx); - await this.handle(transformed); + const authUser = ctx.state.user; + await this.handle(authUser, transformed); // TODO: get template engine handled result and return response by checking API schema return transformed; } // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected async prepare(_ctx: KoaRouterContext) { + protected async prepare(_ctx: KoaContext) { /** * TODO: the graphql need to transform from body. * Therefore, current request and pagination transformer not suitable (need to provide another graphql transform method or class) diff --git a/packages/serve/src/lib/route/route-component/paginationTransformer.ts b/packages/serve/src/lib/route/route-component/paginationTransformer.ts index 775fa3f2..d2d506de 100644 --- a/packages/serve/src/lib/route/route-component/paginationTransformer.ts +++ b/packages/serve/src/lib/route/route-component/paginationTransformer.ts @@ -1,4 +1,4 @@ -import { KoaRouterContext } from '@vulcan-sql/serve/route'; +import { KoaContext } from '@vulcan-sql/serve/models'; import { APISchema, PaginationMode, Pagination } from '@vulcan-sql/core'; import { CursorBasedStrategy, @@ -9,14 +9,14 @@ import { injectable } from 'inversify'; export interface IPaginationTransformer { transform( - ctx: KoaRouterContext, + ctx: KoaContext, apiSchema: APISchema ): Promise; } @injectable() export class PaginationTransformer { - public async transform(ctx: KoaRouterContext, apiSchema: APISchema) { + public async transform(ctx: KoaContext, apiSchema: APISchema) { const { pagination } = apiSchema; if (pagination) { diff --git a/packages/serve/src/lib/route/route-component/requestTransformer.ts b/packages/serve/src/lib/route/route-component/requestTransformer.ts index 0c204780..b66ccdbe 100644 --- a/packages/serve/src/lib/route/route-component/requestTransformer.ts +++ b/packages/serve/src/lib/route/route-component/requestTransformer.ts @@ -7,29 +7,26 @@ import { } from '@vulcan-sql/core'; import { injectable } from 'inversify'; import { assign } from 'lodash'; -import { KoaRouterContext } from './baseRoute'; +import { KoaContext } from '@vulcan-sql/serve/models'; export interface RequestParameters { [name: string]: any; } export interface IRequestTransformer { - transform( - ctx: KoaRouterContext, - apiSchema: APISchema - ): Promise; + transform(ctx: KoaContext, apiSchema: APISchema): Promise; } @injectable() export class RequestTransformer implements IRequestTransformer { public static readonly fieldInMapper: { - [type in FieldInType]: (ctx: KoaRouterContext, fieldName: string) => string; + [type in FieldInType]: (ctx: KoaContext, fieldName: string) => string; } = { - [FieldInType.HEADER]: (ctx: KoaRouterContext, fieldName: string) => + [FieldInType.HEADER]: (ctx: KoaContext, fieldName: string) => ctx.request.header[fieldName] as string, - [FieldInType.QUERY]: (ctx: KoaRouterContext, fieldName: string) => + [FieldInType.QUERY]: (ctx: KoaContext, fieldName: string) => ctx.request.query[fieldName] as string, - [FieldInType.PATH]: (ctx: KoaRouterContext, fieldName: string) => + [FieldInType.PATH]: (ctx: KoaContext, fieldName: string) => ctx.params[fieldName] as string, }; @@ -45,7 +42,7 @@ export class RequestTransformer implements IRequestTransformer { }; public async transform( - ctx: KoaRouterContext, + ctx: KoaContext, apiSchema: APISchema ): Promise { const paramList = await Promise.all( diff --git a/packages/serve/src/lib/route/route-component/restfulRoute.ts b/packages/serve/src/lib/route/route-component/restfulRoute.ts index 0156fe5e..ea94b7c8 100644 --- a/packages/serve/src/lib/route/route-component/restfulRoute.ts +++ b/packages/serve/src/lib/route/route-component/restfulRoute.ts @@ -1,5 +1,5 @@ -import { BaseRoute, KoaRouterContext, RouteOptions } from './baseRoute'; - +import { BaseRoute, RouteOptions } from './baseRoute'; +import { KoaContext } from '@vulcan-sql/serve/models'; export class RestfulRoute extends BaseRoute { public readonly urlPath: string; @@ -9,16 +9,17 @@ export class RestfulRoute extends BaseRoute { this.urlPath = apiSchema.urlPath; } - public async respond(ctx: KoaRouterContext) { + public async respond(ctx: KoaContext) { const transformed = await this.prepare(ctx); - const result = await this.handle(transformed); + const authUser = ctx.state.user; + const result = await this.handle(authUser, transformed); ctx.response.body = { data: result.getData(), columns: result.getColumns(), }; } - protected async prepare(ctx: KoaRouterContext) { + protected async prepare(ctx: KoaContext) { // get request data from context const reqParams = await this.reqTransformer.transform(ctx, this.apiSchema); // validate request format diff --git a/packages/serve/src/models/appConfig.ts b/packages/serve/src/models/appConfig.ts deleted file mode 100644 index 38a47864..00000000 --- a/packages/serve/src/models/appConfig.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ServeConfig } from './serveConfig'; - -export type AppConfig = Omit; diff --git a/packages/serve/src/models/context.ts b/packages/serve/src/models/context.ts new file mode 100644 index 00000000..6793fc6f --- /dev/null +++ b/packages/serve/src/models/context.ts @@ -0,0 +1,7 @@ +import { Next } from 'koa'; +import { RouterContext } from 'koa-router'; +import { AuthUserInfo } from './extensions/authenticator'; + +type KoaContext = RouterContext<{ user: AuthUserInfo }>; + +export { KoaContext, Next }; diff --git a/packages/serve/src/models/extensions/authenticator.ts b/packages/serve/src/models/extensions/authenticator.ts new file mode 100644 index 00000000..e410ff79 --- /dev/null +++ b/packages/serve/src/models/extensions/authenticator.ts @@ -0,0 +1,34 @@ +import { VulcanExtension, ExtensionBase } from '@vulcan-sql/core'; +import { KoaContext, UserAuthOptions } from '@vulcan-sql/serve/models'; +import { TYPES } from '../../containers/types'; + +export interface AuthUserInfo { + name: string; + method: string; + attr: { + [field: string]: string | boolean | number; + }; +} + +export interface AuthResult { + authenticated: boolean; + user?: AuthUserInfo; +} + +export interface IAuthenticator { + authenticate( + usersOptions: Array, + context: KoaContext + ): Promise; +} + +@VulcanExtension(TYPES.Extension_Authenticator) +export abstract class BaseAuthenticator + extends ExtensionBase + implements IAuthenticator +{ + public abstract authenticate( + usersOptions: Array, + context: KoaContext + ): Promise; +} diff --git a/packages/serve/src/models/extensions/index.ts b/packages/serve/src/models/extensions/index.ts index 00c2d76a..1ebacdf1 100644 --- a/packages/serve/src/models/extensions/index.ts +++ b/packages/serve/src/models/extensions/index.ts @@ -1,2 +1,3 @@ export * from './routeMiddleware'; export * from './responseFormatter'; +export * from './authenticator'; diff --git a/packages/serve/src/models/extensions/responseFormatter.ts b/packages/serve/src/models/extensions/responseFormatter.ts index 7aa49143..72a0d672 100644 --- a/packages/serve/src/models/extensions/responseFormatter.ts +++ b/packages/serve/src/models/extensions/responseFormatter.ts @@ -2,7 +2,7 @@ import { DataColumn, ExtensionBase, VulcanExtension } from '@vulcan-sql/core'; import { has } from 'lodash'; import * as Stream from 'stream'; import { TYPES } from '../../containers/types'; -import { KoaRouterContext } from '../../lib/route'; +import { KoaContext } from '@vulcan-sql/serve/models'; export type BodyResponse = { data: Stream.Readable; @@ -20,11 +20,8 @@ export interface IFormatter { columns?: DataColumn[] ): Stream.Readable | Stream.Transform; - toResponse( - stream: Stream.Readable | Stream.Transform, - ctx: KoaRouterContext - ): void; - formatToResponse(ctx: KoaRouterContext): void; + toResponse(stream: Stream.Readable | Stream.Transform, ctx: KoaContext): void; + formatToResponse(ctx: KoaContext): void; } @VulcanExtension(TYPES.Extension_Formatter) @@ -32,7 +29,7 @@ export abstract class BaseResponseFormatter extends ExtensionBase implements IFormatter { - public formatToResponse(ctx: KoaRouterContext) { + public formatToResponse(ctx: KoaContext) { // return empty csv stream data or column is not exist if (!has(ctx.response.body, 'data') || !has(ctx.response.body, 'columns')) { const stream = new Stream.Readable(); @@ -65,6 +62,6 @@ export abstract class BaseResponseFormatter */ public abstract toResponse( stream: Stream.Readable | Stream.Transform, - ctx: KoaRouterContext + ctx: KoaContext ): void; } diff --git a/packages/serve/src/models/extensions/routeMiddleware.ts b/packages/serve/src/models/extensions/routeMiddleware.ts index e7cfa03f..252c72fa 100644 --- a/packages/serve/src/models/extensions/routeMiddleware.ts +++ b/packages/serve/src/models/extensions/routeMiddleware.ts @@ -1,5 +1,5 @@ import { ExtensionBase, VulcanExtension } from '@vulcan-sql/core'; -import { KoaRouterContext, KoaNext } from '@vulcan-sql/serve/route'; +import { KoaContext, Next } from '@vulcan-sql/serve/models'; import { inject } from 'inversify'; import { isUndefined } from 'lodash'; import { TYPES } from '../../containers/types'; @@ -12,10 +12,7 @@ export interface BuiltInMiddlewareConfig