mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 11:25:17 +00:00
Merge pull request #13087 from strapi/menu-logo/update-project-settings-route
Menu logo/update project settings route
This commit is contained in:
commit
d4c9cbfe47
@ -8,6 +8,11 @@ const { ValidationError } = require('@strapi/utils').errors;
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const ee = require('@strapi/strapi/lib/utils/ee');
|
||||
|
||||
const {
|
||||
validateUpdateProjectSettings,
|
||||
validateUpdateProjectSettingsFiles,
|
||||
validateUpdateProjectSettingsImagesDimensions,
|
||||
} = require('../validation/project-settings');
|
||||
const { getService } = require('../utils');
|
||||
|
||||
const PLUGIN_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-_]+$/;
|
||||
@ -43,6 +48,22 @@ module.exports = {
|
||||
return { data: { uuid, hasAdmin } };
|
||||
},
|
||||
|
||||
async updateProjectSettings(ctx) {
|
||||
const projectSettingsService = getService('project-settings');
|
||||
|
||||
const {
|
||||
request: { files, body },
|
||||
} = ctx;
|
||||
|
||||
await validateUpdateProjectSettings(body);
|
||||
await validateUpdateProjectSettingsFiles(files);
|
||||
|
||||
const formatedFiles = await projectSettingsService.parseFilesData(files);
|
||||
await validateUpdateProjectSettingsImagesDimensions(formatedFiles);
|
||||
|
||||
return projectSettingsService.updateProjectSettings({ ...body, ...formatedFiles });
|
||||
},
|
||||
|
||||
async information() {
|
||||
const currentEnvironment = strapi.config.get('environment');
|
||||
const autoReload = strapi.config.get('autoReload', false);
|
||||
|
||||
@ -7,6 +7,20 @@ module.exports = [
|
||||
handler: 'admin.init',
|
||||
config: { auth: false },
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/project-settings',
|
||||
handler: 'admin.updateProjectSettings',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: { actions: ['admin::project-settings.update'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/project-type',
|
||||
|
||||
@ -0,0 +1,294 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
parseFilesData,
|
||||
getProjectSettings,
|
||||
deleteOldFiles,
|
||||
updateProjectSettings,
|
||||
} = require('../project-settings');
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
...jest.requireActual('fs'),
|
||||
createReadStream: () => null,
|
||||
}));
|
||||
|
||||
const storeSet = jest.fn();
|
||||
const providerDelete = jest.fn();
|
||||
global.strapi = {
|
||||
dirs: {
|
||||
public: 'publicDir',
|
||||
},
|
||||
config: {
|
||||
get: () => ({ provider: 'local' }),
|
||||
},
|
||||
plugins: {
|
||||
upload: {
|
||||
provider: {
|
||||
async uploadStream(file) {
|
||||
file.url = `/uploads/${file.hash}`;
|
||||
},
|
||||
delete: providerDelete,
|
||||
},
|
||||
services: {
|
||||
upload: {
|
||||
formatFileInfo: () => ({
|
||||
name: 'filename.png',
|
||||
alternativeText: null,
|
||||
caption: null,
|
||||
hash: 'filename_123',
|
||||
ext: '.png',
|
||||
mime: 'image/png',
|
||||
size: 123,
|
||||
}),
|
||||
},
|
||||
'image-manipulation': {
|
||||
getDimensions: () => ({ width: 100, height: 100 }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
store: () => ({
|
||||
get: () => ({
|
||||
menuLogo: {
|
||||
name: 'name',
|
||||
url: 'file/url',
|
||||
width: 100,
|
||||
height: 100,
|
||||
ext: 'png',
|
||||
size: 123,
|
||||
provider: 'local',
|
||||
},
|
||||
}),
|
||||
set: storeSet,
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Project setting', () => {
|
||||
beforeEach(jest.resetAllMocks);
|
||||
|
||||
describe('parseFilesData', () => {
|
||||
it('Should parse valid files object', async () => {
|
||||
const files = {
|
||||
menuLogo: {
|
||||
size: 123,
|
||||
path: '/tmp/filename_123',
|
||||
name: 'file.png',
|
||||
type: 'image/png',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
menuLogo: {
|
||||
name: 'filename.png',
|
||||
alternativeText: null,
|
||||
caption: null,
|
||||
hash: 'filename_123',
|
||||
ext: '.png',
|
||||
mime: 'image/png',
|
||||
provider: 'local',
|
||||
size: 123,
|
||||
stream: null,
|
||||
width: 100,
|
||||
height: 100,
|
||||
tmpPath: '/tmp/filename_123',
|
||||
},
|
||||
};
|
||||
|
||||
const parsedFiles = await parseFilesData(files);
|
||||
|
||||
expect(parsedFiles).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('Should skip empty files object with no error', async () => {
|
||||
const files = {};
|
||||
const expectedOutput = {};
|
||||
|
||||
const parsedFiles = await parseFilesData(files);
|
||||
expect(parsedFiles).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProjectSettings', () => {
|
||||
it('Should return project settings from store (only the right subset of fields)', async () => {
|
||||
const projectSettings = await getProjectSettings();
|
||||
|
||||
const expectedOutput = {
|
||||
menuLogo: {
|
||||
name: 'name',
|
||||
url: 'file/url',
|
||||
width: 100,
|
||||
height: 100,
|
||||
ext: 'png',
|
||||
size: 123,
|
||||
},
|
||||
};
|
||||
|
||||
expect(projectSettings).toStrictEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOldFiles', () => {
|
||||
it('Does not delete when there was no previous file', async () => {
|
||||
const previousSettings = {
|
||||
menuLogo: null,
|
||||
};
|
||||
|
||||
const newSettings = {
|
||||
menuLogo: {
|
||||
size: 24085,
|
||||
name: 'file.png',
|
||||
type: 'image/png',
|
||||
url: 'file/url',
|
||||
},
|
||||
};
|
||||
|
||||
await deleteOldFiles({ previousSettings, newSettings });
|
||||
|
||||
expect(providerDelete).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Does not delete when there is no new file uploaded', async () => {
|
||||
const previousSettings = {
|
||||
menuLogo: {
|
||||
size: 24085,
|
||||
name: 'file.png',
|
||||
type: 'image/png',
|
||||
provider: 'local',
|
||||
url: 'file/url',
|
||||
},
|
||||
};
|
||||
|
||||
const newSettings = previousSettings;
|
||||
|
||||
await deleteOldFiles({ previousSettings, newSettings });
|
||||
|
||||
expect(providerDelete).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Deletes when inputs are explicitely set to null', async () => {
|
||||
const previousSettings = {
|
||||
menuLogo: {
|
||||
size: 24085,
|
||||
name: 'file.png',
|
||||
type: 'image/png',
|
||||
provider: 'local',
|
||||
url: 'file/url',
|
||||
},
|
||||
};
|
||||
|
||||
const newSettings = { menuLogo: null };
|
||||
|
||||
await deleteOldFiles({ previousSettings, newSettings });
|
||||
|
||||
expect(providerDelete).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Deletes when new files are uploaded', async () => {
|
||||
const previousSettings = {
|
||||
menuLogo: {
|
||||
size: 24085,
|
||||
name: 'file.png',
|
||||
type: 'image/png',
|
||||
provider: 'local',
|
||||
url: 'file/url',
|
||||
hash: '123',
|
||||
},
|
||||
};
|
||||
|
||||
const newSettings = {
|
||||
menuLogo: {
|
||||
...previousSettings.menuLogo,
|
||||
hash: '456',
|
||||
},
|
||||
};
|
||||
|
||||
await deleteOldFiles({ previousSettings, newSettings });
|
||||
|
||||
expect(providerDelete).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProjectSettings', () => {
|
||||
it('Updates the project settings', async () => {
|
||||
const body = {};
|
||||
const files = {
|
||||
menuLogo: {
|
||||
name: 'filename.png',
|
||||
alternativeText: null,
|
||||
caption: null,
|
||||
hash: 'filename_123',
|
||||
ext: '.png',
|
||||
mime: 'image/png',
|
||||
size: 123,
|
||||
stream: null,
|
||||
width: 100,
|
||||
height: 100,
|
||||
tmpPath: '/tmp/filename_123',
|
||||
url: '/uploads/filename_123.png',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
menuLogo: {
|
||||
name: 'filename.png',
|
||||
hash: 'filename_123',
|
||||
url: '/uploads/filename_123.png',
|
||||
width: 100,
|
||||
height: 100,
|
||||
ext: '.png',
|
||||
size: 123,
|
||||
},
|
||||
};
|
||||
|
||||
await updateProjectSettings({ ...body, ...files });
|
||||
|
||||
expect(storeSet).toBeCalledTimes(1);
|
||||
expect(storeSet).toBeCalledWith({
|
||||
key: 'project-settings',
|
||||
value: expectedOutput,
|
||||
});
|
||||
});
|
||||
|
||||
it('Updates the project settings (delete)', async () => {
|
||||
const body = { menuLogo: '' };
|
||||
const files = {};
|
||||
|
||||
const expectedOutput = {
|
||||
menuLogo: null,
|
||||
};
|
||||
|
||||
await updateProjectSettings({ ...body, ...files });
|
||||
|
||||
expect(storeSet).toBeCalledTimes(1);
|
||||
expect(storeSet).toBeCalledWith({
|
||||
key: 'project-settings',
|
||||
value: expectedOutput,
|
||||
});
|
||||
});
|
||||
|
||||
it('Keeps the previous project settings', async () => {
|
||||
const body = {};
|
||||
const files = {};
|
||||
|
||||
const expectedOutput = {
|
||||
menuLogo: {
|
||||
name: 'name',
|
||||
url: 'file/url',
|
||||
width: 100,
|
||||
height: 100,
|
||||
ext: 'png',
|
||||
size: 123,
|
||||
provider: 'local',
|
||||
},
|
||||
};
|
||||
|
||||
await updateProjectSettings({ ...body, ...files });
|
||||
|
||||
expect(storeSet).toBeCalledTimes(1);
|
||||
expect(storeSet).toBeCalledWith({
|
||||
key: 'project-settings',
|
||||
value: expectedOutput,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -13,4 +13,5 @@ module.exports = {
|
||||
auth: require('./auth'),
|
||||
action: require('./action'),
|
||||
'api-token': require('./api-token'),
|
||||
'project-settings': require('./project-settings'),
|
||||
};
|
||||
|
||||
162
packages/core/admin/server/services/project-settings.js
Normal file
162
packages/core/admin/server/services/project-settings.js
Normal file
@ -0,0 +1,162 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const { pick } = require('lodash');
|
||||
|
||||
const PROJECT_SETTINGS_FILE_INPUTS = ['menuLogo'];
|
||||
|
||||
const parseFilesData = async files => {
|
||||
const formatedFilesData = {};
|
||||
|
||||
await Promise.all(
|
||||
PROJECT_SETTINGS_FILE_INPUTS.map(async inputName => {
|
||||
const file = files[inputName];
|
||||
|
||||
// Skip empty file inputs
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getStream = () => fs.createReadStream(file.path);
|
||||
|
||||
// Add formated data for the upload provider
|
||||
formatedFilesData[inputName] = strapi
|
||||
.plugin('upload')
|
||||
.service('upload')
|
||||
.formatFileInfo({
|
||||
filename: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
});
|
||||
|
||||
// Add image dimensions
|
||||
Object.assign(
|
||||
formatedFilesData[inputName],
|
||||
await strapi
|
||||
.plugin('upload')
|
||||
.service('image-manipulation')
|
||||
.getDimensions({ getStream })
|
||||
);
|
||||
|
||||
// Add file path, and stream
|
||||
Object.assign(formatedFilesData[inputName], {
|
||||
stream: getStream(),
|
||||
tmpPath: file.path,
|
||||
provider: strapi.config.get('plugin.upload').provider,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return formatedFilesData;
|
||||
};
|
||||
|
||||
const getProjectSettings = async () => {
|
||||
const store = strapi.store({ type: 'core', name: 'admin' });
|
||||
const projectSettings = await store.get({ key: 'project-settings' });
|
||||
|
||||
// Filter file input fields
|
||||
PROJECT_SETTINGS_FILE_INPUTS.forEach(inputName => {
|
||||
if (!projectSettings[inputName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
projectSettings[inputName] = pick(projectSettings[inputName], [
|
||||
'name',
|
||||
'url',
|
||||
'width',
|
||||
'height',
|
||||
'ext',
|
||||
'size',
|
||||
]);
|
||||
});
|
||||
|
||||
return projectSettings;
|
||||
};
|
||||
|
||||
const uploadFiles = async (files = {}) => {
|
||||
// Call the provider upload function for each file
|
||||
return Promise.all(
|
||||
Object.values(files)
|
||||
.filter(file => file.stream instanceof fs.ReadStream)
|
||||
.map(file => strapi.plugin('upload').provider.uploadStream(file))
|
||||
);
|
||||
};
|
||||
|
||||
const deleteOldFiles = async ({ previousSettings, newSettings }) => {
|
||||
return Promise.all(
|
||||
PROJECT_SETTINGS_FILE_INPUTS.map(async inputName => {
|
||||
// Skip if there was no previous file
|
||||
if (!previousSettings[inputName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if the file was not changed
|
||||
if (
|
||||
newSettings[inputName] &&
|
||||
previousSettings[inputName].hash === newSettings[inputName].hash
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if the file was not uploaded with the current provider
|
||||
if (strapi.config.get('plugin.upload').provider !== previousSettings[inputName].provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There was a previous file and an new file was uploaded
|
||||
// Remove the previous file
|
||||
strapi.plugin('upload').provider.delete(previousSettings[inputName]);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateProjectSettings = async newSettings => {
|
||||
const store = strapi.store({ type: 'core', name: 'admin' });
|
||||
const previousSettings = await store.get({ key: 'project-settings' });
|
||||
const files = pick(newSettings, PROJECT_SETTINGS_FILE_INPUTS);
|
||||
|
||||
await uploadFiles(files);
|
||||
|
||||
PROJECT_SETTINGS_FILE_INPUTS.forEach(inputName => {
|
||||
// If the user input exists but is not a formdata "file" remove it
|
||||
if (newSettings[inputName] !== undefined && !(typeof newSettings[inputName] === 'object')) {
|
||||
newSettings[inputName] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user input is undefined reuse previous setting (do not update field)
|
||||
if (!newSettings[inputName]) {
|
||||
newSettings[inputName] = previousSettings[inputName];
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the file
|
||||
newSettings[inputName] = pick(newSettings[inputName], [
|
||||
'name',
|
||||
'hash',
|
||||
'url',
|
||||
'width',
|
||||
'height',
|
||||
'ext',
|
||||
'size',
|
||||
'provider',
|
||||
]);
|
||||
});
|
||||
|
||||
// No await to proceed asynchronously
|
||||
deleteOldFiles({ previousSettings, newSettings });
|
||||
|
||||
await store.set({
|
||||
key: 'project-settings',
|
||||
value: { ...previousSettings, ...newSettings },
|
||||
});
|
||||
|
||||
return getProjectSettings();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
deleteOldFiles,
|
||||
parseFilesData,
|
||||
getProjectSettings,
|
||||
updateProjectSettings,
|
||||
};
|
||||
2
packages/core/admin/server/utils/index.d.ts
vendored
2
packages/core/admin/server/utils/index.d.ts
vendored
@ -6,6 +6,7 @@ import * as metrics from '../services/metrics';
|
||||
import * as token from '../services/token';
|
||||
import * as auth from '../services/auth';
|
||||
import * as apiToken from '../services/api-token';
|
||||
import * as projectSettings from '../services/project-settings';
|
||||
|
||||
type S = {
|
||||
role: typeof role;
|
||||
@ -16,6 +17,7 @@ type S = {
|
||||
auth: typeof auth;
|
||||
metrics: typeof metrics;
|
||||
'api-token': typeof apiToken;
|
||||
'project-settings': typeof projectSettings;
|
||||
};
|
||||
|
||||
export function getService<T extends keyof S>(name: T): S[T];
|
||||
|
||||
39
packages/core/admin/server/validation/project-settings.js
Normal file
39
packages/core/admin/server/validation/project-settings.js
Normal file
@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
const { yup, validateYupSchemaSync } = require('@strapi/utils');
|
||||
|
||||
const MAX_IMAGE_WIDTH = 750;
|
||||
const MAX_IMAGE_HEIGHT = MAX_IMAGE_WIDTH;
|
||||
const MAX_IMAGE_FILE_SIZE = 1024 * 1024; // 1Mo
|
||||
const ALLOWED_IMAGE_FILE_TYPES = ['image/jpeg', 'image/png', 'image/svg+xml'];
|
||||
|
||||
const updateProjectSettings = yup
|
||||
.object({
|
||||
menuLogo: yup.string(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const updateProjectSettingsFiles = yup
|
||||
.object({
|
||||
menuLogo: yup.object({
|
||||
name: yup.string(),
|
||||
type: yup.string().oneOf(ALLOWED_IMAGE_FILE_TYPES),
|
||||
size: yup.number().max(MAX_IMAGE_FILE_SIZE),
|
||||
}),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const updateProjectSettingsImagesDimensions = yup.object({
|
||||
menuLogo: yup.object({
|
||||
width: yup.number().max(MAX_IMAGE_WIDTH),
|
||||
height: yup.number().max(MAX_IMAGE_HEIGHT),
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
validateUpdateProjectSettings: validateYupSchemaSync(updateProjectSettings),
|
||||
validateUpdateProjectSettingsFiles: validateYupSchemaSync(updateProjectSettingsFiles),
|
||||
validateUpdateProjectSettingsImagesDimensions: validateYupSchemaSync(
|
||||
updateProjectSettingsImagesDimensions
|
||||
),
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user