Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(configuration): Allow app configuration to be validated against …
…a schema
  • Loading branch information
daffl committed Apr 4, 2022
commit 904c05425bf6e6306bb4b34333816971d557fc1c
4 changes: 2 additions & 2 deletions packages/authentication/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NotAuthenticated } from '@feathersjs/errors';
import { createDebug } from '@feathersjs/commons';
import { Application, Params } from '@feathersjs/feathers';
import { IncomingMessage, ServerResponse } from 'http';
import defaultOptions from './options';
import { defaultOptions } from './options';

const debug = createDebug('@feathersjs/authentication/base');

Expand Down Expand Up @@ -167,7 +167,7 @@ export class AuthenticationBase {

/**
* Returns a single strategy by name
*
*
* @param name The strategy name
* @returns The authentication strategy or undefined
*/
Expand Down
1 change: 1 addition & 0 deletions packages/authentication/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
export { AuthenticationBaseStrategy } from './strategy';
export { AuthenticationService } from './service';
export { JWTStrategy } from './jwt';
export { authenticationSettingsSchema } from './options';
107 changes: 105 additions & 2 deletions packages/authentication/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default {
authStrategies: [],
export const defaultOptions = {
authStrategies: [] as string[],
jwtOptions: {
header: { typ: 'access' }, // by default is an access token but can be any type
audience: 'https://yourdomain.com', // The resource server where the token is processed
Expand All @@ -8,3 +8,106 @@ export default {
expiresIn: '1d'
}
};

export const authenticationSettingsSchema = {
type: 'object',
required: ['secret', 'entity', 'authStrategies'],
properties: {
secret: {
type: 'string',
description: 'The JWT signing secret'
},
entity: {
oneOf: [{
type: 'null'
}, {
type: 'string'
}],
description: 'The name of the authentication entity (e.g. user)'
},
entityId: {
type: 'string',
description: 'The name of the authentication entity id property'
},
service: {
type: 'string',
description: 'The path of the entity service'
},
authStrategies: {
type: 'array',
items: { type: 'string' },
description: 'A list of authentication strategy names that are allowed to create JWT access tokens'
},
parseStrategies: {
type: 'array',
items: { type: 'string' },
description: 'A list of authentication strategy names that should parse HTTP headers for authentication information (defaults to `authStrategies`)'
},
jwtOptions: {
type: 'object'
},
jwt: {
type: 'object',
header: {
type: 'string',
default: 'Authorization',
description: 'The HTTP header containing the JWT'
},
schemes: {
type: 'array',
items: { type: 'string' },
description: 'An array of schemes to support'
}
},
local: {
type: 'object',
required: ['usernameField', 'passwordField'],
properties: {
usernameField: {
type: 'string',
description: 'Name of the username field (e.g. `email`)'
},
passwordField: {
type: 'string',
description: 'Name of the password field (e.g. `password`)'
},
hashSize: {
type: 'number',
description: 'The BCrypt salt length'
},
errorMessage: {
type: 'string',
default: 'Invalid login',
description: 'The error message to return on errors'
},
entityUsernameField: {
type: 'string',
description: 'Name of the username field on the entity if authentication request data and entity field names are different'
},
entityPasswordField: {
type: 'string',
description: 'Name of the password field on the entity if authentication request data and entity field names are different'
}
}
},
oauth: {
type: 'object',
properties: {
redirect: {
type: 'string'
},
origins: {
type: 'array',
items: { type: 'string' }
},
defaults: {
type: 'object',
properties: {
key: { type: 'string' },
secret: { type: 'string' }
}
}
}
}
}
} as const;
2 changes: 1 addition & 1 deletion packages/authentication/test/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken';
import { feathers, Application } from '@feathersjs/feathers';
import { memory, Service as MemoryService } from '@feathersjs/memory';

import defaultOptions from '../src/options';
import { defaultOptions } from '../src/options';
import { AuthenticationService } from '../src';

import { Strategy1 } from './fixtures';
Expand Down
1 change: 1 addition & 0 deletions packages/configuration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"dependencies": {
"@feathersjs/commons": "^5.0.0-pre.17",
"@feathersjs/feathers": "^5.0.0-pre.17",
"@feathersjs/schema": "^5.0.0-pre.17",
"@types/config": "^0.0.41",
"config": "^3.3.7"
},
Expand Down
14 changes: 12 additions & 2 deletions packages/configuration/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Application } from '@feathersjs/feathers';
import { Application, ApplicationHookContext, NextFunction } from '@feathersjs/feathers';
import { createDebug } from '@feathersjs/commons';
import { Schema } from '@feathersjs/schema'
import config from 'config';

const debug = createDebug('@feathersjs/configuration');

export = function init () {
export = function init (schema?: Schema<any>) {
return (app?: Application) => {
if (!app) {
return config;
Expand All @@ -18,6 +19,15 @@ export = function init () {
app.set(name, value);
});

if (schema) {
app.hooks({
setup: [async (context: ApplicationHookContext, next: NextFunction) => {
await schema.validate(context.app.settings);
await next();
}]
})
}

return config;
};
}
47 changes: 44 additions & 3 deletions packages/configuration/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { strict as assert } from 'assert';
import { feathers, Application } from '@feathersjs/feathers';
import plugin from '../src';
import { Ajv, schema } from '@feathersjs/schema';
import configuration from '../src';

describe('@feathersjs/configuration', () => {
const app: Application = feathers().configure(plugin());
const app: Application = feathers().configure(configuration());

it('initialized app with default.json', () => {
assert.equal(app.get('port'), 3030);
Expand All @@ -15,9 +16,49 @@ describe('@feathersjs/configuration', () => {
});

it('works when called directly', () => {
const fn = plugin();
const fn = configuration();
const conf = fn() as any;

assert.strictEqual(conf.port, 3030);
});

it('errors on .setup when a schema is passed and the configuration is invalid', async () => {
const configurationSchema = schema({
$id: 'ConfigurationSchema',
additionalProperties: false,
type: 'object',
properties: {
port: { type: 'number' },
deep: {
type: 'object',
properties: {
base: {
type: 'boolean'
}
}
},
array: {
type: 'array',
items: { type: 'string' }
},
nullish: {
type: 'string'
}
}
} as const, new Ajv());

const schemaApp = feathers().configure(configuration(configurationSchema))

await assert.rejects(() => schemaApp.setup(), {
data: [{
instancePath: '/nullish',
keyword: 'type',
message: 'must be string',
params: {
type: 'string'
},
schemaPath: '#/properties/nullish/type'
}]
});
});
});
3 changes: 2 additions & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"scripts": {
"prepublish": "npm run compile",
"compile": "shx rm -rf lib/ && tsc",
"test": "mocha --config ../../.mocharc.json --recursive test/**.test.ts test/**/*.test.ts"
"mocha": "mocha --config ../../.mocharc.json --recursive test/**.test.ts test/**/*.test.ts",
"test": "npm run compile && npm run mocha"
},
"directories": {
"lib": "lib"
Expand Down
5 changes: 3 additions & 2 deletions packages/schema/src/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BadRequest } from '@feathersjs/errors';
import { Schema } from './schema';

export type PropertyResolver<T, V, C> = (
value: V|undefined,
Expand All @@ -14,7 +15,7 @@ export type PropertyResolverMap<T, C> = {
export interface ResolverConfig<T, C> {
// TODO this should be `Schema<any>` but has recently produced an error, see
// https://github.com/ThomasAribart/json-schema-to-ts/issues/53
schema?: any,
schema?: Schema<T>,
validate?: 'before'|'after'|false,
properties: PropertyResolverMap<T, C>
}
Expand Down Expand Up @@ -71,7 +72,7 @@ export class Resolver<T, C> {

// Not the most elegant but better performance
await Promise.all(propertyList.map(async name => {
const value = data[name];
const value = (data as any)[name];

if (resolvers[name]) {
try {
Expand Down
16 changes: 11 additions & 5 deletions packages/schema/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ import Ajv, { AsyncValidateFunction, ValidateFunction } from 'ajv';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { BadRequest } from '@feathersjs/errors';

export const AJV = new Ajv({
export const DEFAULT_AJV = new Ajv({
coerceTypes: true
});

export { Ajv };

export type JSONSchemaDefinition = JSONSchema & { $id: string, $async?: boolean };

export class Schema<S extends JSONSchemaDefinition> {
export interface Schema<T> {
validate <X = T> (...args: Parameters<ValidateFunction<X>>): Promise<X>;
}

export class SchemaWrapper<S extends JSONSchemaDefinition> implements Schema<FromSchema<S>> {
ajv: Ajv;
validator: AsyncValidateFunction;
readonly _type!: FromSchema<S>;

constructor (public definition: S, ajv: Ajv = AJV) {
constructor (public definition: S, ajv: Ajv = DEFAULT_AJV) {
this.ajv = ajv;
this.validator = this.ajv.compile({
$async: true,
Expand All @@ -36,6 +42,6 @@ export class Schema<S extends JSONSchemaDefinition> {
}
}

export function schema <S extends JSONSchemaDefinition> (definition: S, ajv: Ajv = AJV) {
return new Schema(definition, ajv);
export function schema <S extends JSONSchemaDefinition> (definition: S, ajv: Ajv = DEFAULT_AJV) {
return new SchemaWrapper(definition, ajv);
}