add utils.validate and replace sanitize usage

This commit is contained in:
Ben Irvin 2023-08-10 15:24:35 +02:00
parent 2fd0ed49fd
commit 995473d959
55 changed files with 1166 additions and 71 deletions

View File

@ -4,8 +4,9 @@ const _ = require('lodash');
const { cloneDeep, isPlainObject } = require('lodash/fp');
const { subject: asSubject } = require('@casl/ability');
const createSanitizeHelpers = require('./sanitize');
const createValidateHelpers = require('./validate');
const { buildStrapiQuery, buildCaslQuery } = require('./query-builers');
const { buildStrapiQuery, buildCaslQuery } = require('./query-builders');
module.exports = ({ ability, action, model }) => ({
ability,
@ -48,4 +49,5 @@ module.exports = ({ ability, action, model }) => ({
},
...createSanitizeHelpers({ action, ability, model }),
...createValidateHelpers({ action, ability, model }),
});

View File

@ -19,7 +19,7 @@ const {
cloneDeep,
} = require('lodash/fp');
const { contentTypes, traverseEntity, sanitize, pipeAsync, traverse } = require('@strapi/utils');
const { contentTypes, traverseEntity, sanitize, pipeAsync } = require('@strapi/utils');
const { removePassword } = require('@strapi/utils').sanitize.visitors;
const { ADMIN_USER_ALLOWED_FIELDS } = require('../../../domain/user');
@ -45,7 +45,10 @@ const STATIC_FIELDS = [ID_ATTRIBUTE];
module.exports = ({ action, ability, model }) => {
const schema = strapi.getModel(model);
const { sanitizePasswords } = sanitize.sanitizers;
const { allowedFields } = sanitize.visitors;
const { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate, traverseQueryFields } =
sanitize.traversals;
const createSanitizeQuery = (options = {}) => {
const { fields } = options;
@ -54,10 +57,10 @@ module.exports = ({ action, ability, model }) => {
const permittedFields = fields.shouldIncludeAll ? null : getQueryFields(fields.permitted);
const sanitizeFilters = pipeAsync(
traverse.traverseQueryFilters(allowedFields(permittedFields), { schema }),
traverse.traverseQueryFilters(omitDisallowedAdminUserFields, { schema }),
traverse.traverseQueryFilters(removePassword, { schema }),
traverse.traverseQueryFilters(
traverseQueryFilters(allowedFields(permittedFields), { schema }),
traverseQueryFilters(omitDisallowedAdminUserFields, { schema }),
traverseQueryFilters(removePassword, { schema }),
traverseQueryFilters(
({ key, value }, { remove }) => {
if (isObject(value) && isEmpty(value)) {
remove(key);
@ -68,10 +71,10 @@ module.exports = ({ action, ability, model }) => {
);
const sanitizeSort = pipeAsync(
traverse.traverseQuerySort(allowedFields(permittedFields), { schema }),
traverse.traverseQuerySort(omitDisallowedAdminUserFields, { schema }),
traverse.traverseQuerySort(removePassword, { schema }),
traverse.traverseQuerySort(
traverseQuerySort(allowedFields(permittedFields), { schema }),
traverseQuerySort(omitDisallowedAdminUserFields, { schema }),
traverseQuerySort(removePassword, { schema }),
traverseQuerySort(
({ key, attribute, value }, { remove }) => {
if (!isScalarAttribute(attribute) && isEmpty(value)) {
remove(key);
@ -82,14 +85,14 @@ module.exports = ({ action, ability, model }) => {
);
const sanitizePopulate = pipeAsync(
traverse.traverseQueryPopulate(allowedFields(permittedFields), { schema }),
traverse.traverseQueryPopulate(omitDisallowedAdminUserFields, { schema }),
traverse.traverseQueryPopulate(removePassword, { schema })
traverseQueryPopulate(allowedFields(permittedFields), { schema }),
traverseQueryPopulate(omitDisallowedAdminUserFields, { schema }),
traverseQueryPopulate(removePassword, { schema })
);
const sanitizeFields = pipeAsync(
traverse.traverseQueryFields(allowedFields(permittedFields), { schema }),
traverse.traverseQueryFields(removePassword, { schema })
traverseQueryFields(allowedFields(permittedFields), { schema }),
traverseQueryFields(removePassword, { schema })
);
return async (query) => {
@ -128,7 +131,7 @@ module.exports = ({ action, ability, model }) => {
// Remove not allowed fields (RBAC)
traverseEntity(allowedFields(permittedFields), { schema }),
// Remove all fields of type 'password'
sanitize.sanitizers.sanitizePasswords(schema)
sanitizePasswords(schema)
);
};
@ -270,5 +273,6 @@ module.exports = ({ action, ability, model }) => {
sanitizeOutput: wrapSanitize(createSanitizeOutput),
sanitizeInput: wrapSanitize(createSanitizeInput),
sanitizeQuery: wrapSanitize(createSanitizeQuery),
validateQuery: wrapSanitize(createSanitizeQuery),
};
};

View File

@ -0,0 +1,222 @@
'use strict';
const { subject: asSubject, detectSubjectType } = require('@casl/ability');
const { permittedFieldsOf } = require('@casl/ability/extra');
const {
defaults,
omit,
isArray,
isEmpty,
isNil,
flatMap,
some,
prop,
uniq,
intersection,
getOr,
isObject,
cloneDeep,
} = require('lodash/fp');
const { contentTypes, traverseEntity, validate, pipeAsync } = require('@strapi/utils');
const { removePassword } = require('@strapi/utils').validate.visitors;
const { ADMIN_USER_ALLOWED_FIELDS } = require('../../../domain/user');
const { constants, isScalarAttribute, getNonVisibleAttributes, getWritableAttributes } =
contentTypes;
const {
ID_ATTRIBUTE,
CREATED_AT_ATTRIBUTE,
UPDATED_AT_ATTRIBUTE,
PUBLISHED_AT_ATTRIBUTE,
CREATED_BY_ATTRIBUTE,
UPDATED_BY_ATTRIBUTE,
} = constants;
const COMPONENT_FIELDS = ['__component'];
const STATIC_FIELDS = [ID_ATTRIBUTE];
module.exports = ({ action, ability, model }) => {
const schema = strapi.getModel(model);
const { allowedFields } = validate.visitors;
const { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate, traverseQueryFields } =
validate.traversals;
const createValidateQuery = (options = {}) => {
const { fields } = options;
// TODO: sanitize relations to admin users in all sanitizers
const permittedFields = fields.shouldIncludeAll ? null : getQueryFields(fields.permitted);
const validateFilters = pipeAsync(
traverseQueryFilters(allowedFields(permittedFields), { schema }),
traverseQueryFilters(omitDisallowedAdminUserFields, { schema }),
traverseQueryFilters(removePassword, { schema }),
traverseQueryFilters(
({ key, value }, { remove }) => {
if (isObject(value) && isEmpty(value)) {
remove(key);
}
},
{ schema }
)
);
const validateSort = pipeAsync(
traverseQuerySort(allowedFields(permittedFields), { schema }),
traverseQuerySort(omitDisallowedAdminUserFields, { schema }),
traverseQuerySort(removePassword, { schema }),
traverseQuerySort(
({ key, attribute, value }, { remove }) => {
if (!isScalarAttribute(attribute) && isEmpty(value)) {
remove(key);
}
},
{ schema }
)
);
const validatePopulate = pipeAsync(
traverseQueryPopulate(allowedFields(permittedFields), { schema }),
traverseQueryPopulate(omitDisallowedAdminUserFields, { schema }),
traverseQueryPopulate(removePassword, { schema })
);
const validateFields = pipeAsync(
traverseQueryFields(allowedFields(permittedFields), { schema }),
traverseQueryFields(removePassword, { schema })
);
return async (query) => {
const validatedQuery = cloneDeep(query);
if (query.filters) {
Object.assign(validatedQuery, { filters: await validateFilters(query.filters) });
}
if (query.sort) {
Object.assign(validatedQuery, { sort: await validateSort(query.sort) });
}
if (query.populate) {
Object.assign(validatedQuery, { populate: await validatePopulate(query.populate) });
}
if (query.fields) {
Object.assign(validatedQuery, { fields: await validateFields(query.fields) });
}
return validatedQuery;
};
};
const createValidateInput = (options = {}) => {
const { fields } = options;
const permittedFields = fields.shouldIncludeAll ? null : getInputFields(fields.permitted);
return pipeAsync(
// Remove fields hidden from the admin
traverseEntity(omitHiddenFields, { schema }),
// Remove not allowed fields (RBAC)
traverseEntity(allowedFields(permittedFields), { schema }),
// Remove roles from createdBy & updateBy fields
omitCreatorRoles
);
};
const wrapValidate = (createSanitizeFunction) => {
const wrappedValidate = async (data, options = {}) => {
if (isArray(data)) {
return Promise.all(data.map((entity) => wrappedValidate(entity, options)));
}
const { subject, action: actionOverride } = getDefaultOptions(data, options);
const permittedFields = permittedFieldsOf(ability, actionOverride, subject, {
fieldsFrom: (rule) => rule.fields || [],
});
const hasAtLeastOneRegistered = some(
(fields) => !isNil(fields),
flatMap(prop('fields'), ability.rulesFor(actionOverride, detectSubjectType(subject)))
);
const shouldIncludeAllFields = isEmpty(permittedFields) && !hasAtLeastOneRegistered;
const sanitizeOptions = {
...options,
fields: {
shouldIncludeAll: shouldIncludeAllFields,
permitted: permittedFields,
hasAtLeastOneRegistered,
},
};
const sanitizeFunction = createSanitizeFunction(sanitizeOptions);
return sanitizeFunction(data);
};
return wrappedValidate;
};
const getDefaultOptions = (data, options) => {
return defaults({ subject: asSubject(model, data), action }, options);
};
/**
* Omit creator fields' (createdBy & updatedBy) roles from the admin API responses
*/
const omitCreatorRoles = omit([`${CREATED_BY_ATTRIBUTE}.roles`, `${UPDATED_BY_ATTRIBUTE}.roles`]);
/**
* Visitor used to remove hidden fields from the admin API responses
*/
const omitHiddenFields = ({ key, schema }, { remove }) => {
const isHidden = getOr(false, ['config', 'attributes', key, 'hidden'], schema);
if (isHidden) {
remove(key);
}
};
/**
* Visitor used to omit disallowed fields from the admin users entities & avoid leaking sensitive information
*/
const omitDisallowedAdminUserFields = ({ key, attribute, schema }, { remove }) => {
if (schema.uid === 'admin::user' && attribute && !ADMIN_USER_ALLOWED_FIELDS.includes(key)) {
remove(key);
}
};
const getInputFields = (fields = []) => {
const nonVisibleAttributes = getNonVisibleAttributes(schema);
const writableAttributes = getWritableAttributes(schema);
const nonVisibleWritableAttributes = intersection(nonVisibleAttributes, writableAttributes);
return uniq([
...fields,
...STATIC_FIELDS,
...COMPONENT_FIELDS,
...nonVisibleWritableAttributes,
]);
};
const getQueryFields = (fields = []) => {
return uniq([
...fields,
...STATIC_FIELDS,
...COMPONENT_FIELDS,
CREATED_AT_ATTRIBUTE,
UPDATED_AT_ATTRIBUTE,
PUBLISHED_AT_ATTRIBUTE,
]);
};
return {
validateQuery: wrapValidate(createValidateQuery),
validateInput: wrapValidate(createValidateInput),
};
};

View File

@ -44,6 +44,17 @@ const createPermissionChecker =
});
};
const validateQuery = (query, { action = ACTIONS.read } = {}) => {
return permissionsManager.validateQuery(query, { subject: model, action });
};
const validateInput = (action, data, entity) => {
return permissionsManager.validateInput(data, {
subject: entity ? toSubject(entity) : model,
action,
});
};
const sanitizeCreateInput = (data) => sanitizeInput(ACTIONS.create, data);
const sanitizeUpdateInput = (entity) => (data) => sanitizeInput(ACTIONS.update, data, entity);
@ -82,6 +93,9 @@ const createPermissionChecker =
sanitizeQuery,
sanitizeCreateInput,
sanitizeUpdateInput,
// Validators
validateQuery,
validateInput,
// Queries Builder
sanitizedQuery,
};

View File

@ -223,7 +223,7 @@ const getDeepPopulateDraftCount = (uid) => {
const getQueryPopulate = async (uid, query) => {
let populateQuery = {};
await strapiUtils.traverse.traverseQueryFilters(
await strapiUtils.sanitize.traversals.traverseQueryFilters(
/**
*
* @param {Object} param0

View File

@ -19,7 +19,7 @@ const createCollectionTypeController = ({ contentType }) => {
* @return {Object|Array}
*/
async find(ctx) {
const sanitizedQuery = await this.sanitizeQuery(ctx);
const sanitizedQuery = await this.validateQuery(ctx);
const { results, pagination } = await strapi.service(uid).find(sanitizedQuery);
const sanitizedResults = await this.sanitizeOutput(results, ctx);
return this.transformResponse(sanitizedResults, { pagination });

View File

@ -2,7 +2,7 @@
const { getOr } = require('lodash/fp');
const { contentTypes, sanitize } = require('@strapi/utils');
const { contentTypes, sanitize, validate } = require('@strapi/utils');
const { transformResponse } = require('./transform');
const createSingleTypeController = require('./single-type');
@ -35,6 +35,18 @@ const createController = ({ contentType }) => {
return sanitize.contentAPI.query(ctx.query, contentType, { auth });
},
validateQuery(ctx) {
const auth = getAuthFromKoaContext(ctx);
return validate.contentAPI.query(ctx.query, contentType, { auth });
},
validateInput(data, ctx) {
const auth = getAuthFromKoaContext(ctx);
return validate.contentAPI.input(data, contentType, { auth });
},
};
let ctrl;

View File

@ -18,7 +18,7 @@ const createSingleTypeController = ({ contentType }) => {
* @return {Object|Array}
*/
async find(ctx) {
const sanitizedQuery = await this.sanitizeQuery(ctx);
const sanitizedQuery = await this.validateQuery(ctx);
const entity = await strapi.service(uid).find(sanitizedQuery);
const sanitizedEntity = await this.sanitizeOutput(entity, ctx);

View File

@ -12,6 +12,8 @@ export interface Base {
sanitizeOutput<TData>(data: TData, ctx: ExtendableContext): Promise<TData>;
sanitizeInput<TData>(data: TData, ctx: ExtendableContext): Promise<TData>;
sanitizeQuery<TData>(data: TData, ctx: ExtendableContext): Promise<TData>;
validateQuery<TData>(data: TData, ctx: ExtendableContext): Promise<TData>;
validateInput<TData>(data: TData, ctx: ExtendableContext): Promise<TData>;
}
/**

View File

@ -25,7 +25,7 @@ module.exports = {
}
const pmQuery = pm.addPermissionsQueryTo(merge(defaultQuery, ctx.query));
const query = await pm.sanitizeQuery(pmQuery);
const query = await pm.validateQuery(pmQuery);
const { results: files, pagination } = await getService('upload').findPage(query);

View File

@ -16,7 +16,7 @@ module.exports = {
model: FOLDER_MODEL_UID,
});
const query = await permissionsManager.sanitizeQuery(ctx.query);
const query = await permissionsManager.validateQuery(ctx.query);
const { results } = await strapi.entityService.findWithRelationCountsPage(FOLDER_MODEL_UID, {
...defaultsDeep(
{
@ -49,7 +49,7 @@ module.exports = {
model: FOLDER_MODEL_UID,
});
const query = await permissionsManager.sanitizeQuery(ctx.query);
const query = await permissionsManager.validateQuery(ctx.query);
const results = await strapi.entityService.findWithRelationCounts(FOLDER_MODEL_UID, {
...defaultsDeep(
{

View File

@ -6,7 +6,7 @@ const { getService } = require('../utils');
const { FILE_MODEL_UID } = require('../constants');
const validateUploadBody = require('./validation/content-api/upload');
const { sanitize } = utils;
const { sanitize, validate } = utils;
const { ValidationError } = utils.errors;
const sanitizeOutput = (data, ctx) => {
@ -15,16 +15,16 @@ const sanitizeOutput = (data, ctx) => {
return sanitize.contentAPI.output(data, schema, { auth });
};
const sanitizeQuery = (data, ctx) => {
const validateQuery = (data, ctx) => {
const schema = strapi.getModel(FILE_MODEL_UID);
const { auth } = ctx.state;
return sanitize.contentAPI.query(data, schema, { auth });
return validate.contentAPI.query(data, schema, { auth });
};
module.exports = {
async find(ctx) {
const sanitizedParams = await sanitizeQuery(ctx.query, ctx);
const sanitizedParams = await validateQuery(ctx.query, ctx);
const files = await getService('upload').findMany(sanitizedParams);
@ -36,7 +36,7 @@ module.exports = {
params: { id },
} = ctx;
const sanitizedParams = await sanitizeQuery(ctx.query, ctx);
const sanitizedParams = await validateQuery(ctx.query, ctx);
const file = await getService('upload').findOne(id, sanitizedParams.populate);
if (!file) {

View File

@ -32,6 +32,7 @@ import * as hooks from './hooks';
import providerFactory from './provider-factory';
import * as pagination from './pagination';
import sanitize from './sanitize';
import validate from './validate';
import traverseEntity from './traverse-entity';
import { pipeAsync, mapAsync, reduceAsync, forEachAsync } from './async';
import convertQueryParams from './convert-query-params';
@ -78,6 +79,7 @@ export = {
providerFactory,
pagination,
sanitize,
validate,
traverseEntity,
pipeAsync,
mapAsync,

View File

@ -4,11 +4,12 @@ import { isArray, cloneDeep } from 'lodash/fp';
import { getNonWritableAttributes } from '../content-types';
import { pipeAsync } from '../async';
import * as visitors from './visitors';
import * as visitors from '../traverse/visitors';
import * as sanitizers from './sanitizers';
import traverseEntity, { Data } from '../traverse-entity';
import { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate } from '../traverse';
import * as traversals from './traversals';
import { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate } from './traversals';
import { Model } from '../types';
export interface Options {
@ -156,4 +157,5 @@ export default {
contentAPI,
sanitizers,
visitors,
traversals,
};

View File

@ -9,14 +9,14 @@ import {
traverseQuerySort,
traverseQueryPopulate,
traverseQueryFields,
} from '../traverse';
} from './traversals';
import {
removePassword,
removePrivate,
removeDynamicZones,
removeMorphToRelations,
} from './visitors';
} from '../traverse/visitors';
import { isOperator } from '../operators';
import type { Model } from '../types';

View File

@ -0,0 +1,4 @@
export { default as traverseQueryFilters } from './query-filters';
export { default as traverseQuerySort } from './query-sort';
export { default as traverseQueryPopulate } from './query-populate';
export { default as traverseQueryFields } from './query-fields';

View File

@ -1,6 +1,6 @@
import { curry, isArray, isString, eq, trim, constant } from 'lodash/fp';
import traverseFactory from './factory';
import traverseFactory from '../../traverse/factory';
const isStringArray = (value: unknown): value is string[] =>
isArray(value) && value.every(isString);

View File

@ -1,6 +1,6 @@
import { curry, isObject, isEmpty, isArray, isNil, cloneDeep, omit } from 'lodash/fp';
import traverseFactory from './factory';
import traverseFactory from '../../traverse/factory';
const isObj = (value: unknown): value is Record<string, unknown> => isObject(value);

View File

@ -13,9 +13,9 @@ import {
omit,
} from 'lodash/fp';
import traverseFactory from './factory';
import { Attribute } from '../types';
import { isMorphToRelationalAttribute } from '../content-types';
import traverseFactory from '../../traverse/factory';
import { Attribute } from '../../types';
import { isMorphToRelationalAttribute } from '../../content-types';
const isKeyword = (keyword: string) => {
return ({ key, attribute }: { key: string; attribute: Attribute }) => {

View File

@ -13,7 +13,7 @@ import {
cloneDeep,
} from 'lodash/fp';
import traverseFactory from './factory';
import traverseFactory from '../../traverse/factory';
const ORDERS = { asc: 'asc', desc: 'desc' };
const ORDER_VALUES = Object.values(ORDERS);

View File

@ -73,7 +73,7 @@ interface CommonHandler<AttributeType = Attribute> {
handler(ctx: Context<AttributeType>, opts: Pick<TransformUtils, 'set' | 'recurse'>): void;
}
interface TransformUtils {
export interface TransformUtils {
remove(key: string): void;
set(key: string, valeu: unknown): void;
recurse: Traverse;

View File

@ -1,5 +1 @@
export { default as factory } from './factory';
export { default as traverseQueryFilters } from './query-filters';
export { default as traverseQuerySort } from './query-sort';
export { default as traverseQueryPopulate } from './query-populate';
export { default as traverseQueryFields } from './query-fields';

View File

@ -1,5 +1,5 @@
import { isArray, isNil, toPath } from 'lodash/fp';
import type { Visitor } from '../../traverse/factory';
import type { Visitor } from '../factory';
export default (allowedFields: string[] | null = null): Visitor =>
({ key, path: { attribute: path } }, { remove }) => {

View File

@ -1,5 +1,5 @@
import { isDynamicZoneAttribute } from '../../content-types';
import type { Visitor } from '../../traverse/factory';
import { Visitor } from '../factory';
const visitor: Visitor = ({ key, attribute }, { remove }) => {
if (isDynamicZoneAttribute(attribute)) {

View File

@ -1,5 +1,5 @@
import { isMorphToRelationalAttribute } from '../../content-types';
import type { Visitor } from '../../traverse/factory';
import type { Visitor } from '../factory';
const visitor: Visitor = ({ key, attribute }, { remove }) => {
if (isMorphToRelationalAttribute(attribute)) {

View File

@ -1,4 +1,4 @@
import type { Visitor } from '../../traverse/factory';
import type { Visitor } from '../factory';
const visitor: Visitor = ({ key, attribute }, { remove }) => {
if (attribute?.type === 'password') {

View File

@ -1,5 +1,5 @@
import { isPrivateAttribute } from '../../content-types';
import type { Visitor } from '../../traverse/factory';
import type { Visitor } from '../factory';
const visitor: Visitor = ({ schema, key, attribute }, { remove }) => {
if (!attribute) {

View File

@ -1,5 +1,5 @@
import * as contentTypeUtils from '../../content-types';
import type { Visitor } from '../../traverse/factory';
import type { Visitor } from '../factory';
const ACTIONS_TO_VERIFY = ['find'];
const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = contentTypeUtils.constants;

View File

@ -1,5 +1,5 @@
import { isArray } from 'lodash/fp';
import type { Visitor } from '../../traverse/factory';
import type { Visitor } from '../factory';
export default (restrictedFields: string[] | null = null): Visitor =>
({ key, path: { attribute: path } }, { remove }) => {

View File

@ -0,0 +1,138 @@
import { CurriedFunction1 } from 'lodash';
import { isArray, cloneDeep } from 'lodash/fp';
import { getNonWritableAttributes } from '../content-types';
import { pipeAsync } from '../async';
import * as visitors from '../traverse/visitors';
import * as validators from './validators';
import traverseEntity, { Data } from '../traverse-entity';
import * as traversals from './traversals';
import { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate } from './traversals';
import { Model } from '../types';
export interface Options {
auth?: unknown;
}
interface Validator {
(schema: Model): CurriedFunction1<Data, Promise<Data>>;
}
export interface ValidateFunc {
(data: unknown, schema: Model, options?: Options): Promise<unknown>;
}
const createContentAPIValidators = () => {
const validateInput: ValidateFunc = (data: unknown, schema: Model, { auth } = {}) => {
if (isArray(data)) {
return Promise.all(data.map((entry) => validateInput(entry, schema, { auth })));
}
const nonWritableAttributes = getNonWritableAttributes(schema);
const transforms = [
// Remove non writable attributes
traverseEntity(visitors.restrictedFields(nonWritableAttributes), { schema }),
];
if (auth) {
// Remove restricted relations
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
}
// Apply sanitizers from registry if exists
strapi.validators
.get('content-api.input')
.forEach((validator: Validator) => transforms.push(validator(schema)));
return pipeAsync(...transforms)(data as Data);
};
const validateQuery = async (
query: Record<string, unknown>,
schema: Model,
{ auth }: Options = {}
) => {
const { filters, sort, fields, populate } = query;
const validatedQuery = cloneDeep(query);
if (filters) {
Object.assign(validatedQuery, { filters: await validateFilters(filters, schema, { auth }) });
}
if (sort) {
Object.assign(validatedQuery, { sort: await validateSort(sort, schema, { auth }) });
}
if (fields) {
Object.assign(validatedQuery, { fields: await validateFields(fields, schema) });
}
if (populate) {
Object.assign(validatedQuery, { populate: await validatePopulate(populate, schema) });
}
return validatedQuery;
};
const validateFilters: ValidateFunc = (filters, schema: Model, { auth } = {}) => {
if (isArray(filters)) {
return Promise.all(filters.map((filter) => validateFilters(filter, schema, { auth })));
}
const transforms = [validators.defaultSanitizeFilters(schema)];
if (auth) {
transforms.push(traverseQueryFilters(visitors.removeRestrictedRelations(auth), { schema }));
}
return pipeAsync(...transforms)(filters);
};
const validateSort: ValidateFunc = (sort, schema: Model, { auth } = {}) => {
const transforms = [validators.defaultSanitizeSort(schema)];
if (auth) {
transforms.push(traverseQuerySort(visitors.removeRestrictedRelations(auth), { schema }));
}
return pipeAsync(...transforms)(sort);
};
const validateFields: ValidateFunc = (fields, schema: Model) => {
const transforms = [validators.defaultSanitizeFields(schema)];
return pipeAsync(...transforms)(fields);
};
const validatePopulate: ValidateFunc = (populate, schema: Model, { auth } = {}) => {
const transforms = [validators.defaultSanitizePopulate(schema)];
if (auth) {
transforms.push(traverseQueryPopulate(visitors.removeRestrictedRelations(auth), { schema }));
}
return pipeAsync(...transforms)(populate);
};
return {
input: validateInput,
query: validateQuery,
filters: validateFilters,
sort: validateSort,
fields: validateFields,
populate: validatePopulate,
};
};
const contentAPI = createContentAPIValidators();
export default {
contentAPI,
validators,
visitors,
traversals,
};

View File

@ -0,0 +1,4 @@
export { default as traverseQueryFilters } from './query-filters';
export { default as traverseQuerySort } from './query-sort';
export { default as traverseQueryPopulate } from './query-populate';
export { default as traverseQueryFields } from './query-fields';

View File

@ -0,0 +1,39 @@
import { curry, isArray, isString, eq, trim, constant } from 'lodash/fp';
import traverseFactory from '../../traverse/factory';
import { ValidationError } from '../../errors';
const isStringArray = (value: unknown): value is string[] =>
isArray(value) && value.every(isString);
const fields = traverseFactory()
// Interecept array of strings
.intercept(isStringArray, async (visitor, options, fields, { recurse }) => {
return Promise.all(fields.map((field) => recurse(visitor, options, field)));
})
// Return wildcards as is
.intercept((value): value is string => eq('*', value), constant('*'))
// Parse string values
// Since we're parsing strings only, each value should be an attribute name (and it's value, undefined),
// thus it shouldn't be possible to set a new value, and get should return the whole data if key === data
.parse(isString, () => ({
transform: trim,
remove(key, data) {
throw new ValidationError(`Invalid parameter ${key}`);
},
set(_key, _value, data) {
return data;
},
keys(data) {
return [data];
},
get(key, data) {
return key === data ? data : undefined;
},
}));
export default curry(fields.traverse);

View File

@ -0,0 +1,95 @@
import { curry, isObject, isEmpty, isArray, isNil, cloneDeep, omit } from 'lodash/fp';
import traverseFactory from '../../traverse/factory';
import { ValidationError } from '../../errors';
const isObj = (value: unknown): value is Record<string, unknown> => isObject(value);
const filters = traverseFactory()
.intercept(
// Intercept filters arrays and apply the traversal to each one individually
isArray,
async (visitor, options, filters, { recurse }) => {
return Promise.all(
filters.map((filter, i) => {
// In filters, only operators such as $and, $in, $notIn or $or and implicit operators like [...]
// can have a value array, thus we can update the raw path but not the attribute one
const newPath = options.path
? { ...options.path, raw: `${options.path.raw}[${i}]` }
: options.path;
return recurse(visitor, { ...options, path: newPath }, filter);
})
// todo: move that to the visitors
).then((res) => res.filter((val) => !(isObject(val) && isEmpty(val))));
}
)
.intercept(
// Ignore non object filters and return the value as-is
(filters): filters is unknown => !isObject(filters),
(_, __, filters) => {
return filters;
}
)
// Parse object values
.parse(isObj, () => ({
transform: cloneDeep,
remove(key, data) {
throw new ValidationError(`Invalid parameter ${key}`);
},
set(key, value, data) {
return { ...data, [key]: value };
},
keys(data) {
return Object.keys(data);
},
get(key, data) {
return data[key];
},
}))
// Ignore null or undefined values
.ignore(({ value }) => isNil(value))
// Recursion on operators (non attributes)
.on(
({ attribute }) => isNil(attribute),
async ({ key, visitor, path, value, schema }, { set, recurse }) => {
set(key, await recurse(visitor, { schema, path }, value));
}
)
// Handle relation recursion
.onRelation(async ({ key, attribute, visitor, path, value }, { set, recurse }) => {
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
if (isMorphRelation) {
return;
}
const targetSchemaUID = attribute.target;
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
.onComponent(async ({ key, attribute, visitor, path, value }, { set, recurse }) => {
const targetSchema = strapi.getModel(attribute.component);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle media recursion
.onMedia(async ({ key, visitor, path, value }, { set, recurse }) => {
const targetSchemaUID = 'plugin::upload.file';
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
});
export default curry(filters.traverse);

View File

@ -0,0 +1,230 @@
import {
curry,
isString,
isArray,
isEmpty,
split,
isObject,
trim,
isNil,
cloneDeep,
join,
first,
omit,
} from 'lodash/fp';
import traverseFactory from '../../traverse/factory';
import { Attribute } from '../../types';
import { isMorphToRelationalAttribute } from '../../content-types';
import { ValidationError } from '../../errors';
const isKeyword = (keyword: string) => {
return ({ key, attribute }: { key: string; attribute: Attribute }) => {
return !attribute && keyword === key;
};
};
const isStringArray = (value: unknown): value is string[] =>
isArray(value) && value.every(isString);
const isWildCardConstant = (value: unknown): value is '*' => value === '*';
const isObj = (value: unknown): value is Record<string, unknown> => isObject(value);
const populate = traverseFactory()
// Array of strings ['foo', 'foo.bar'] => map(recurse), then filter out empty items
.intercept(isStringArray, async (visitor, options, populate, { recurse }) => {
const visitedPopulate = await Promise.all(
populate.map((nestedPopulate) => recurse(visitor, options, nestedPopulate))
);
return visitedPopulate.filter((item) => !isNil(item));
})
// Transform wildcard populate to an exhaustive list of attributes to populate.
.intercept(isWildCardConstant, (visitor, options, _data, { recurse }) => {
const attributes = options.schema?.attributes;
// This should never happen, but adding the check in
// case this method is called with wrong parameters
if (!attributes) {
return '*';
}
const parsedPopulate = Object.entries(attributes)
// Get the list of all attributes that can be populated
.filter(([, value]) => ['relation', 'component', 'dynamiczone', 'media'].includes(value.type))
// Only keep the attributes key
.reduce((acc, [key]) => ({ ...acc, [key]: true }), {});
return recurse(visitor, options, parsedPopulate);
})
// Parse string values
.parse(isString, () => {
const tokenize = split('.');
const recompose = join('.');
return {
transform: trim,
remove(key, data) {
throw new ValidationError(`Invalid parameter ${key}`);
},
set(key, value, data) {
const [root] = tokenize(data);
if (root !== key) {
return data;
}
return isNil(value) || isEmpty(value) ? root : `${root}.${value}`;
},
keys(data) {
const v = first(tokenize(data));
return v ? [v] : [];
},
get(key, data) {
const [root, ...rest] = tokenize(data);
return key === root ? recompose(rest) : undefined;
},
};
})
// Parse object values
.parse(isObj, () => ({
transform: cloneDeep,
remove(key, data) {
throw new ValidationError(`Invalid parameter ${key}`);
},
set(key, value, data) {
return { ...data, [key]: value };
},
keys(data) {
return Object.keys(data);
},
get(key, data) {
return data[key];
},
}))
.ignore(({ key, attribute }) => {
return ['sort', 'filters', 'fields'].includes(key) && !attribute;
})
.on(
// Handle recursion on populate."populate"
isKeyword('populate'),
async ({ key, visitor, path, value, schema }, { set, recurse }) => {
const newValue = await recurse(visitor, { schema, path }, value);
set(key, newValue);
}
)
.on(isKeyword('on'), async ({ key, visitor, path, value }, { set, recurse }) => {
const newOn: Record<string, unknown> = {};
if (!isObj(value)) {
return;
}
for (const [uid, subPopulate] of Object.entries(value)) {
const model = strapi.getModel(uid);
const newPath = { ...path, raw: `${path.raw}[${uid}]` };
newOn[uid] = await recurse(visitor, { schema: model, path: newPath }, subPopulate);
}
set(key, newOn);
})
// Handle populate on relation
.onRelation(async ({ key, value, attribute, visitor, path, schema }, { set, recurse }) => {
if (isNil(value)) {
return;
}
if (isMorphToRelationalAttribute(attribute)) {
// Don't traverse values that cannot be parsed
if (!isObject(value) || !('on' in value && isObject(value?.on))) {
return;
}
// If there is a populate fragment defined, traverse it
const newValue = await recurse(visitor, { schema, path }, { on: value?.on });
set(key, { on: newValue });
}
const targetSchemaUID = attribute.target;
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle populate on media
.onMedia(async ({ key, path, visitor, value }, { recurse, set }) => {
if (isNil(value)) {
return;
}
const targetSchemaUID = 'plugin::upload.file';
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle populate on components
.onComponent(async ({ key, value, visitor, path, attribute }, { recurse, set }) => {
if (isNil(value)) {
return;
}
const targetSchema = strapi.getModel(attribute.component);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle populate on dynamic zones
.onDynamicZone(async ({ key, value, attribute, schema, visitor, path }, { set, recurse }) => {
if (isNil(value)) {
return;
}
if (isObject(value)) {
const { components } = attribute;
const newValue = {};
// Handle legacy DZ params
let newProperties: unknown = omit('on', value);
for (const componentUID of components) {
const componentSchema = strapi.getModel(componentUID);
newProperties = await recurse(visitor, { schema: componentSchema, path }, newProperties);
}
Object.assign(newValue, newProperties);
// Handle new morph fragment syntax
if ('on' in value && value.on) {
const newOn = await recurse(visitor, { schema, path }, { on: value.on });
// Recompose both syntaxes
Object.assign(newValue, newOn);
}
set(key, newValue);
} else {
const newValue = await recurse(visitor, { schema, path }, value);
set(key, newValue);
}
});
export default curry(populate.traverse);

View File

@ -0,0 +1,166 @@
import {
curry,
isString,
isObject,
map,
trim,
split,
isEmpty,
flatten,
pipe,
isNil,
first,
cloneDeep,
} from 'lodash/fp';
import traverseFactory from '../../traverse/factory';
import { ValidationError } from '../../errors';
const ORDERS = { asc: 'asc', desc: 'desc' };
const ORDER_VALUES = Object.values(ORDERS);
const isSortOrder = (value: string) => ORDER_VALUES.includes(value.toLowerCase());
const isStringArray = (value: unknown): value is string[] =>
Array.isArray(value) && value.every(isString);
const isObjectArray = (value: unknown): value is object[] =>
Array.isArray(value) && value.every(isObject);
const isNestedSorts = (value: unknown): value is string =>
isString(value) && value.split(',').length > 1;
const isObj = (value: unknown): value is Record<string, unknown> => isObject(value);
const sort = traverseFactory()
.intercept(
// String with chained sorts (foo,bar,foobar) => split, map(recurse), then recompose
isNestedSorts,
async (visitor, options, sort, { recurse }) => {
return Promise.all(
sort
.split(',')
.map(trim)
.map((nestedSort) => recurse(visitor, options, nestedSort))
).then((res) => res.filter((part) => !isEmpty(part)).join(','));
}
)
.intercept(
// Array of strings ['foo', 'foo,bar'] => map(recurse), then filter out empty items
isStringArray,
async (visitor, options, sort, { recurse }) => {
return Promise.all(sort.map((nestedSort) => recurse(visitor, options, nestedSort))).then(
(res) => res.filter((nestedSort) => !isEmpty(nestedSort))
);
}
)
.intercept(
// Array of objects [{ foo: 'asc' }, { bar: 'desc', baz: 'asc' }] => map(recurse), then filter out empty items
isObjectArray,
async (visitor, options, sort, { recurse }) => {
return Promise.all(sort.map((nestedSort) => recurse(visitor, options, nestedSort))).then(
(res) => res.filter((nestedSort) => !isEmpty(nestedSort))
);
}
)
// Parse string values
.parse(isString, () => {
const tokenize = pipe(split('.'), map(split(':')), flatten);
const recompose = (parts: string[]) => {
if (parts.length === 0) {
return undefined;
}
return parts.reduce((acc, part) => {
if (isEmpty(part)) {
return acc;
}
if (acc === '') {
return part;
}
return isSortOrder(part) ? `${acc}:${part}` : `${acc}.${part}`;
}, '');
};
return {
transform: trim,
remove(key, data) {
throw new ValidationError(`Invalid parameter ${key}`);
},
set(key, value, data) {
const [root] = tokenize(data);
if (root !== key) {
return data;
}
return isNil(value) ? root : `${root}.${value}`;
},
keys(data) {
const v = first(tokenize(data));
return v ? [v] : [];
},
get(key, data) {
const [root, ...rest] = tokenize(data);
return key === root ? recompose(rest) : undefined;
},
};
})
// Parse object values
.parse(isObj, () => ({
transform: cloneDeep,
remove(key, data) {
throw new ValidationError(`Invalid parameter ${key}`);
},
set(key, value, data) {
return { ...data, [key]: value };
},
keys(data) {
return Object.keys(data);
},
get(key, data) {
return data[key];
},
}))
// Handle deep sort on relation
.onRelation(async ({ key, value, attribute, visitor, path }, { set, recurse }) => {
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
if (isMorphRelation) {
return;
}
const targetSchemaUID = attribute.target;
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle deep sort on media
.onMedia(async ({ key, path, visitor, value }, { recurse, set }) => {
const targetSchemaUID = 'plugin::upload.file';
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle deep sort on components
.onComponent(async ({ key, value, visitor, path, attribute }, { recurse, set }) => {
const targetSchema = strapi.getModel(attribute.component);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
});
export default curry(sort.traverse);

View File

@ -0,0 +1,163 @@
import { curry, isEmpty, isNil, isArray, isObject } from 'lodash/fp';
import { pipeAsync } from '../async';
import traverseEntity, { Data } from '../traverse-entity';
import { isScalarAttribute } from '../content-types';
import {
traverseQueryFilters,
traverseQuerySort,
traverseQueryPopulate,
traverseQueryFields,
} from './traversals';
import {
removePassword,
removePrivate,
removeDynamicZones,
removeMorphToRelations,
} from '../traverse/visitors';
import { isOperator } from '../operators';
import type { Model } from '../types';
const sanitizePasswords = (schema: Model) => async (entity: Data) => {
return traverseEntity(removePassword, { schema }, entity);
};
const defaultSanitizeOutput = async (schema: Model, entity: Data) => {
return traverseEntity(
(...args) => {
removePassword(...args);
removePrivate(...args);
},
{ schema },
entity
);
};
const defaultSanitizeFilters = curry((schema: Model, filters: unknown) => {
return pipeAsync(
// Remove keys that are not attributes or valid operators
traverseQueryFilters(
({ key, attribute }, { remove }) => {
const isAttribute = !!attribute;
if (!isAttribute && !isOperator(key) && key !== 'id') {
remove(key);
}
},
{ schema }
),
// Remove dynamic zones from filters
traverseQueryFilters(removeDynamicZones, { schema }),
// Remove morpTo relations from filters
traverseQueryFilters(removeMorphToRelations, { schema }),
// Remove passwords from filters
traverseQueryFilters(removePassword, { schema }),
// Remove private from filters
traverseQueryFilters(removePrivate, { schema }),
// Remove empty objects
traverseQueryFilters(
({ key, value }, { remove }) => {
if (isObject(value) && isEmpty(value)) {
remove(key);
}
},
{ schema }
)
)(filters);
});
const defaultSanitizeSort = curry((schema: Model, sort: unknown) => {
return pipeAsync(
// Remove non attribute keys
traverseQuerySort(
({ key, attribute }, { remove }) => {
// ID is not an attribute per se, so we need to make
// an extra check to ensure we're not removing it
if (key === 'id') {
return;
}
if (!attribute) {
remove(key);
}
},
{ schema }
),
// Remove dynamic zones from sort
traverseQuerySort(removeDynamicZones, { schema }),
// Remove morpTo relations from sort
traverseQuerySort(removeMorphToRelations, { schema }),
// Remove private from sort
traverseQuerySort(removePrivate, { schema }),
// Remove passwords from filters
traverseQuerySort(removePassword, { schema }),
// Remove keys for empty non-scalar values
traverseQuerySort(
({ key, attribute, value }, { remove }) => {
if (!isScalarAttribute(attribute) && isEmpty(value)) {
remove(key);
}
},
{ schema }
)
)(sort);
});
const defaultSanitizeFields = curry((schema: Model, fields: unknown) => {
return pipeAsync(
// Only keep scalar attributes
traverseQueryFields(
({ key, attribute }, { remove }) => {
if (isNil(attribute) || !isScalarAttribute(attribute)) {
remove(key);
}
},
{ schema }
),
// Remove private fields
traverseQueryFields(removePrivate, { schema }),
// Remove password fields
traverseQueryFields(removePassword, { schema }),
// Remove nil values from fields array
(value) => (isArray(value) ? value.filter((field) => !isNil(field)) : value)
)(fields);
});
const defaultSanitizePopulate = curry((schema: Model, populate: unknown) => {
return pipeAsync(
traverseQueryPopulate(
async ({ key, value, schema, attribute }, { set }) => {
if (attribute) {
return;
}
if (key === 'sort') {
set(key, await defaultSanitizeSort(schema, value));
}
if (key === 'filters') {
set(key, await defaultSanitizeFilters(schema, value));
}
if (key === 'fields') {
set(key, await defaultSanitizeFields(schema, value));
}
},
{ schema }
),
// Remove private fields
traverseQueryPopulate(removePrivate, { schema })
)(populate);
});
export {
sanitizePasswords,
defaultSanitizeOutput,
defaultSanitizeFilters,
defaultSanitizeSort,
defaultSanitizeFields,
defaultSanitizePopulate,
};

View File

@ -5,7 +5,7 @@ const { omit, isNil } = require('lodash/fp');
const utils = require('@strapi/utils');
const { sanitize } = utils;
const { sanitize, validate } = utils;
const { NotFoundError } = utils.errors;
module.exports = ({ strapi }) => {
@ -53,7 +53,7 @@ module.exports = ({ strapi }) => {
.get('content-api')
.buildMutationsResolvers({ contentType });
const sanitizedQuery = await sanitize.contentAPI.query(
const sanitizedQuery = await validate.contentAPI.query(
omit(['data', 'files'], transformedArgs),
contentType,
{
@ -91,7 +91,7 @@ module.exports = ({ strapi }) => {
.get('content-api')
.buildMutationsResolvers({ contentType });
const sanitizedQuery = await sanitize.contentAPI.query(transformedArgs, contentType, {
const sanitizedQuery = await validate.contentAPI.query(transformedArgs, contentType, {
auth: ctx?.state?.auth,
});

View File

@ -4,7 +4,7 @@ const { get } = require('lodash/fp');
const utils = require('@strapi/utils');
const { sanitize, pipeAsync } = utils;
const { sanitize, validate, pipeAsync } = utils;
const { ApplicationError } = utils.errors;
module.exports = ({ strapi }) => {
@ -41,7 +41,7 @@ module.exports = ({ strapi }) => {
usePagination: true,
});
const sanitizedQuery = await sanitize.contentAPI.query(transformedArgs, targetContentType, {
const sanitizedQuery = await validate.contentAPI.query(transformedArgs, targetContentType, {
auth,
});

View File

@ -1,6 +1,6 @@
'use strict';
const { sanitize } = require('@strapi/utils');
const { validate } = require('@strapi/utils');
module.exports = ({ strapi }) => ({
buildComponentResolver({ contentTypeUID, attributeName }) {
@ -13,7 +13,7 @@ module.exports = ({ strapi }) => ({
const component = strapi.getModel(componentName);
const transformedArgs = transformArgs(args, { contentType: component, usePagination: true });
const sanitizedQuery = await sanitize.contentAPI.query(transformedArgs, contentType, {
const sanitizedQuery = await validate.contentAPI.query(transformedArgs, contentType, {
auth: ctx?.state?.auth,
});
return strapi.entityService.load(contentTypeUID, parent, attributeName, sanitizedQuery);

View File

@ -1,7 +1,7 @@
'use strict';
const { pick } = require('lodash/fp');
const { sanitize } = require('@strapi/utils');
const { validate } = require('@strapi/utils');
const pickCreateArgs = pick(['params', 'data', 'files']);
@ -25,7 +25,7 @@ module.exports = ({ strapi }) => ({
async delete(parent, args, ctx) {
const { id, ...rest } = args;
const sanitizedQuery = sanitize.contentAPI.query(rest, contentType, {
const sanitizedQuery = validate.contentAPI.query(rest, contentType, {
auth: ctx?.state?.auth,
});
return strapi.entityService.delete(uid, id, sanitizedQuery);

View File

@ -1,7 +1,7 @@
'use strict';
const { omit } = require('lodash/fp');
const { sanitize } = require('@strapi/utils');
const { validate } = require('@strapi/utils');
module.exports = ({ strapi }) => ({
buildQueriesResolvers({ contentType }) {
@ -9,7 +9,7 @@ module.exports = ({ strapi }) => ({
return {
async find(parent, args, ctx) {
const sanitizedQuery = await sanitize.contentAPI.query(args, contentType, {
const sanitizedQuery = await validate.contentAPI.query(args, contentType, {
auth: ctx?.state?.auth,
});
@ -17,7 +17,7 @@ module.exports = ({ strapi }) => ({
},
async findOne(parent, args, ctx) {
const sanitizedQuery = await sanitize.contentAPI.query(args, contentType, {
const sanitizedQuery = await validate.contentAPI.query(args, contentType, {
auth: ctx?.state?.auth,
});

View File

@ -1,7 +1,7 @@
'use strict';
const { objectType } = require('nexus');
const { sanitize } = require('@strapi/utils');
const { validate } = require('@strapi/utils');
module.exports = ({ strapi }) => {
const { RESPONSE_COLLECTION_META_TYPE_NAME, PAGINATION_TYPE_NAME } = strapi
@ -27,7 +27,7 @@ module.exports = ({ strapi }) => {
const safeLimit = Math.max(limit, 1);
const contentType = strapi.getModel(resourceUID);
const sanitizedQuery = await sanitize.contentAPI.query(args, contentType, {
const sanitizedQuery = await validate.contentAPI.query(args, contentType, {
auth: ctx?.state?.auth,
});
const total = await strapi.entityService.count(resourceUID, sanitizedQuery);

View File

@ -11,7 +11,7 @@ const utils = require('@strapi/utils');
const { getService } = require('../utils');
const { validateCreateUserBody, validateUpdateUserBody } = require('./validation/user');
const { sanitize } = utils;
const { sanitize, validate } = utils;
const { ApplicationError, ValidationError, NotFoundError } = utils.errors;
const sanitizeOutput = async (user, ctx) => {
@ -21,11 +21,11 @@ const sanitizeOutput = async (user, ctx) => {
return sanitize.contentAPI.output(user, schema, { auth });
};
const sanitizeQuery = async (query, ctx) => {
const validateQuery = async (query, ctx) => {
const schema = strapi.getModel('plugin::users-permissions.user');
const { auth } = ctx.state;
return sanitize.contentAPI.query(query, schema, { auth });
return validate.contentAPI.query(query, schema, { auth });
};
module.exports = {
@ -143,7 +143,7 @@ module.exports = {
* @return {Object|Array}
*/
async find(ctx) {
const sanitizedQuery = await sanitizeQuery(ctx.query, ctx);
const sanitizedQuery = await validateQuery(ctx.query, ctx);
const users = await getService('user').fetchAll(sanitizedQuery);
ctx.body = await Promise.all(users.map((user) => sanitizeOutput(user, ctx)));
@ -155,7 +155,7 @@ module.exports = {
*/
async findOne(ctx) {
const { id } = ctx.params;
const sanitizedQuery = await sanitizeQuery(ctx.query, ctx);
const sanitizedQuery = await validateQuery(ctx.query, ctx);
let data = await getService('user').fetch(id, sanitizedQuery);
@ -171,7 +171,7 @@ module.exports = {
* @return {Number}
*/
async count(ctx) {
const sanitizedQuery = await sanitizeQuery(ctx.query, ctx);
const sanitizedQuery = await validateQuery(ctx.query, ctx);
ctx.body = await getService('user').count(sanitizedQuery);
},
@ -201,7 +201,7 @@ module.exports = {
return ctx.unauthorized();
}
const sanitizedQuery = await sanitizeQuery(query, ctx);
const sanitizedQuery = await validateQuery(query, ctx);
const user = await getService('user').fetch(authUser.id, sanitizedQuery);
ctx.body = await sanitizeOutput(user, ctx);