apostrophe 3.52.0 → 3.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -2
- package/defaults.js +1 -0
- package/index.js +3 -2
- package/lib/check-if-conditions.js +44 -0
- package/lib/moog-require.js +23 -1
- package/modules/@apostrophecms/admin-bar/index.js +30 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +4 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +14 -8
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +8 -2
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +4 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +1 -0
- package/modules/@apostrophecms/doc/index.js +13 -7
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +36 -22
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +35 -27
- package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
- package/modules/@apostrophecms/i18n/index.js +49 -2
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +16 -1
- package/modules/@apostrophecms/login/index.js +5 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +37 -40
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +3 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +4 -5
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposFocusMixin.js +91 -0
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposModalTabsMixin.js +16 -4
- package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +9 -3
- package/modules/@apostrophecms/piece-type/index.js +1 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +1 -1
- package/modules/@apostrophecms/schema/index.js +13 -0
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +3 -10
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -15
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +20 -13
- package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +164 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +141 -0
- package/modules/@apostrophecms/settings/index.js +627 -0
- package/modules/@apostrophecms/settings/ui/apos/apps/TheAposSettings.js +8 -0
- package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +162 -0
- package/modules/@apostrophecms/settings/ui/apos/logic/AposSettingsManager.js +169 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +10 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +23 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellLabels.vue +1 -7
- package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +136 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +6 -6
- package/modules/@apostrophecms/ui/ui/apos/mixins/AposCellMixin.js +9 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_admin.scss +9 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +5 -1
- package/modules/@apostrophecms/user/index.js +30 -3
- package/package.json +1 -1
- package/test/i18n.js +168 -0
- package/test/settings.js +544 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
// Enable users to manage their personal settings (user record).
|
|
2
|
+
//
|
|
3
|
+
// ## Options
|
|
4
|
+
//
|
|
5
|
+
// `subforms`
|
|
6
|
+
//
|
|
7
|
+
// An object with subform configurations. The key is the subform name, the value
|
|
8
|
+
// is the subform configuration described below. Subforms rendered on the client
|
|
9
|
+
// side have two modes - preview and edit. The initial mode is preview. The configuration
|
|
10
|
+
// provides the necessary information for both modes.
|
|
11
|
+
//
|
|
12
|
+
// ```js
|
|
13
|
+
// subforms: {
|
|
14
|
+
// // The subform name
|
|
15
|
+
// name: {
|
|
16
|
+
// // Required subform fields, shown in the order specified. The fields should
|
|
17
|
+
// // exist in the user schema. The information is used in edit mode only.
|
|
18
|
+
// // Currently supported system user fields are 'adminLocale' and 'password'.
|
|
19
|
+
// // Keep in mind 'adminLocale' is available only if the `apostrophecms/i18n` module
|
|
20
|
+
// // has the appropriate configuration.
|
|
21
|
+
// fields: [ 'firstName', 'lastName' ],
|
|
22
|
+
// // Optional subform label. Used in both preview and edit mode.
|
|
23
|
+
// label: 'Profile',
|
|
24
|
+
// // Optional subform help text. It is rendered instead
|
|
25
|
+
// // of the subform preview value in preview mode only.
|
|
26
|
+
// help: 'Your full name',
|
|
27
|
+
// // The subform value rendered in preview mode, but only if `help` option is not provided.
|
|
28
|
+
// // A string or i18n key / template can be specified.
|
|
29
|
+
// // If not specified, the UI will attempt to generate a
|
|
30
|
+
// // preview value based on the subform schema and field values (space separated).
|
|
31
|
+
// preview: '{{ firstName }} {{ lastName }}',
|
|
32
|
+
// // In effect ONLY if `preview` and `help` options are not present. Provide a custom,
|
|
33
|
+
// // already registered (admin UI) component to render the subform preview value.
|
|
34
|
+
// // The subform config object and current field values will be passed as props.
|
|
35
|
+
// previewComponent: 'MyComponent',
|
|
36
|
+
// // Optional protection type. Currently allowed values are `password`
|
|
37
|
+
// // and `true` (alias of `password`). If specified, the subform will be
|
|
38
|
+
// // protected by the user current password.
|
|
39
|
+
// protection: true,
|
|
40
|
+
// // Optional flag to indicate that the subform should be reloaded after save.
|
|
41
|
+
// reload: true
|
|
42
|
+
// }
|
|
43
|
+
// }
|
|
44
|
+
// ```
|
|
45
|
+
//
|
|
46
|
+
// `groups`
|
|
47
|
+
//
|
|
48
|
+
// An object with group configurations. The key is the group name, the value
|
|
49
|
+
// is the group configuration, described below. Groups are used to organize
|
|
50
|
+
// subforms in the settings modal (tabs). If no groups are configured, a single group
|
|
51
|
+
// named "ungrouped" will be created. The order of the groups is respected.
|
|
52
|
+
//
|
|
53
|
+
// ```js
|
|
54
|
+
// groups: {
|
|
55
|
+
// // The group name
|
|
56
|
+
// account: {
|
|
57
|
+
// // The group label.
|
|
58
|
+
// label: 'Account',
|
|
59
|
+
// // The subforms that belong to the group. The order is respected.
|
|
60
|
+
// subforms: [ 'name', 'password' ]
|
|
61
|
+
// }
|
|
62
|
+
// }
|
|
63
|
+
// ```
|
|
64
|
+
//
|
|
65
|
+
// ## API
|
|
66
|
+
//
|
|
67
|
+
// Add a protected field to the system protected fields list. This will ensure
|
|
68
|
+
// that any subform containing that field will be ALWAYS protected by
|
|
69
|
+
// the user's current password. It is recommended to use this method in the
|
|
70
|
+
// `apostrophe:modulesRegistered` event handler.
|
|
71
|
+
// `self.apos.settings.addProtectedField(fieldName, protectionType)`
|
|
72
|
+
//
|
|
73
|
+
// Add a forbidden field to the forbidden fields list. This will ensure that
|
|
74
|
+
// the field will not be allowed in any subform. It is recommended to use this
|
|
75
|
+
// method in the `apostrophe:modulesRegistered` event handler.
|
|
76
|
+
// `self.apos.settings.addForbiddenField(fieldName)`
|
|
77
|
+
//
|
|
78
|
+
// Add a field to the reload after save fields list. This will ensure that
|
|
79
|
+
// the page will be reloaded after subform containing the field is saved.
|
|
80
|
+
// It is recommended to use this method in the `apostrophe:modulesRegistered`
|
|
81
|
+
// event handler.
|
|
82
|
+
// `self.apos.settings.addReloadAfterSaveField(fieldName)`
|
|
83
|
+
//
|
|
84
|
+
// ## UI
|
|
85
|
+
//
|
|
86
|
+
// An example of a custom `previewComponent` with the core components explained
|
|
87
|
+
// can be found in the relevant PR:
|
|
88
|
+
// https://github.com/apostrophecms/apostrophe/pull/4236
|
|
89
|
+
const { klona } = require('klona');
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
options: {
|
|
93
|
+
alias: 'settings',
|
|
94
|
+
subforms: {},
|
|
95
|
+
groups: {}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
init(self) {
|
|
99
|
+
// List of all allowed protection types and their aliases (`subform.protection: type`).
|
|
100
|
+
// The key is the type or alias, the value is the actual type (always a string).
|
|
101
|
+
// All subforms `protection` prop will be converted to the actual type.
|
|
102
|
+
// Invalid protection type will panic.
|
|
103
|
+
self.protectionTypes = {
|
|
104
|
+
// Protection type to be used if protected is simply set to `true`.
|
|
105
|
+
true: 'password',
|
|
106
|
+
password: 'password'
|
|
107
|
+
// TODO phase 3
|
|
108
|
+
// email: 'email'
|
|
109
|
+
};
|
|
110
|
+
// Collection of fieldName: protectionType objects for system forced protected fields.
|
|
111
|
+
// The order is important, the first match is used (first have higher priority).
|
|
112
|
+
// If there are multiple fields in the subform, having a system protected field,
|
|
113
|
+
// the first match from this list wins. If there is specifically `password` field
|
|
114
|
+
// in the subform, the schema will be completely replaced with the auto-generated
|
|
115
|
+
// password schema.
|
|
116
|
+
// Do not modify this object directly, use
|
|
117
|
+
// `self.apos.settings.addProtectedField(fieldName, protectionType)` instead.
|
|
118
|
+
self.systemProtectedFields = {
|
|
119
|
+
password: self.protectionTypes.password
|
|
120
|
+
// TODO phase 3
|
|
121
|
+
// username: self.protectionTypes.password,
|
|
122
|
+
// email: self.protectionTypes.email
|
|
123
|
+
};
|
|
124
|
+
// Completely forbidden fields, they are not allowed in the subforms.
|
|
125
|
+
// Do not modify this array directly, use
|
|
126
|
+
// `self.apos.settings.addForbiddenField(fieldName)` instead.
|
|
127
|
+
self.systemForbiddenFields = [
|
|
128
|
+
'role',
|
|
129
|
+
'disabled',
|
|
130
|
+
// TODO remove in phase 3
|
|
131
|
+
'username',
|
|
132
|
+
'email'
|
|
133
|
+
];
|
|
134
|
+
// Fields that should trigger reload after saving.
|
|
135
|
+
// Do not modify this array directly, use
|
|
136
|
+
// `self.apos.settings.addReloadAfterSaveField(fieldName)` instead.
|
|
137
|
+
self.systemReloadAfterSaveFields = [
|
|
138
|
+
'adminLocale'
|
|
139
|
+
];
|
|
140
|
+
self.userSchema = [];
|
|
141
|
+
self.subforms = [];
|
|
142
|
+
self.initSubforms();
|
|
143
|
+
self.enableBrowserData();
|
|
144
|
+
self.addToAdminBar();
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
handlers(self) {
|
|
148
|
+
return {
|
|
149
|
+
'apostrophe:modulesRegistered': {
|
|
150
|
+
addModal() {
|
|
151
|
+
self.addSettingsModal();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
methods(self) {
|
|
158
|
+
return {
|
|
159
|
+
// Public API method.
|
|
160
|
+
// Add a protected field to the system protected fields list.
|
|
161
|
+
// Modules can add their own protected fields here
|
|
162
|
+
// via 'apostrophe:modulesRegistered' event handler:
|
|
163
|
+
// ```js
|
|
164
|
+
// self.apos.settings.addProtectedField('myField', true);
|
|
165
|
+
// self.apos.settings.addProtectedField('myField', 'password');
|
|
166
|
+
// self.apos.settings.addProtectedField('myField', 'email');
|
|
167
|
+
// ```
|
|
168
|
+
addProtectedField(fieldName, protectionType) {
|
|
169
|
+
if (!self.protectionTypes[protectionType]) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`[@apostrophecms/settings] Attempt to add a protected field "${fieldName}" with invalid protection type "${protectionType}".`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (!self.systemProtectedFields[fieldName]) {
|
|
175
|
+
self.systemProtectedFields[fieldName] = self.protectionTypes[protectionType];
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Public API method.
|
|
180
|
+
// Add a forbidden field to the forbidden fields list.
|
|
181
|
+
// Modules can add their own forbidden fields here
|
|
182
|
+
// via 'apostrophe:modulesRegistered' event handler:
|
|
183
|
+
// `self.apos.settings.addForbiddenField('myField');`
|
|
184
|
+
addForbiddenField(fieldName) {
|
|
185
|
+
if (!self.systemForbiddenFields.includes(fieldName)) {
|
|
186
|
+
self.systemForbiddenFields.push(fieldName);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Public API method.
|
|
191
|
+
// Add a field to the reload after save fields list.
|
|
192
|
+
// Modules can add their own reload after save fields here
|
|
193
|
+
// via 'apostrophe:modulesRegistered' event handler:
|
|
194
|
+
// `self.apos.settings.addReloadAfterSaveField('myField');`
|
|
195
|
+
addReloadAfterSaveField(fieldName) {
|
|
196
|
+
if (!self.systemReloadAfterSaveFields.includes(fieldName)) {
|
|
197
|
+
self.systemReloadAfterSaveFields.push(fieldName);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
hasSchema() {
|
|
202
|
+
return self.userSchema.length > 0;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Initialize the subforms configuration.
|
|
206
|
+
initSubforms() {
|
|
207
|
+
self.userSchema = self.inferUserSchema();
|
|
208
|
+
|
|
209
|
+
for (const [ name, config ] of Object.entries(self.options.subforms)) {
|
|
210
|
+
// Don't allow malformed subform.fields, the only required prop.
|
|
211
|
+
if (!Array.isArray(config.fields) || config.fields.length === 0) {
|
|
212
|
+
throw new Error(`[@apostrophecms/settings] The subform "${name}" must have at least one field.`);
|
|
213
|
+
}
|
|
214
|
+
// Don't allow malformed subform.protection.
|
|
215
|
+
if (config.protection && !self.protectionTypes[config.protection]) {
|
|
216
|
+
throw new Error(`[@apostrophecms/settings] The protection type "${config.protection}" is not valid.`);
|
|
217
|
+
}
|
|
218
|
+
if (config.protection) {
|
|
219
|
+
config.protection = self.protectionTypes[config.protection];
|
|
220
|
+
}
|
|
221
|
+
// Auto reload after save.
|
|
222
|
+
config.reload = config.reload || self.systemReloadAfterSaveFields
|
|
223
|
+
.some(field => config.fields.includes(field));
|
|
224
|
+
// No one is allowed to set the flag but us.
|
|
225
|
+
delete config._passwordChangeForm;
|
|
226
|
+
const schema = self.getSubformSchema(name);
|
|
227
|
+
|
|
228
|
+
self.subforms.push({
|
|
229
|
+
...config,
|
|
230
|
+
name,
|
|
231
|
+
schema,
|
|
232
|
+
// constrain the fields to the ones that are actually in the user
|
|
233
|
+
fields: schema.map(field => field.name)
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.initGroups();
|
|
238
|
+
this.enhanceSubforms();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Initialize groups based on the configuration given. Fallback to
|
|
242
|
+
// a single group (Other) if none is configured. Move fields that
|
|
243
|
+
// are not in a group to the "Ungrouped" group.
|
|
244
|
+
// This method requires initialized self.subforms.
|
|
245
|
+
initGroups() {
|
|
246
|
+
if (!self.hasSchema()) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Contains properly sorted fields by groups
|
|
250
|
+
const newSubforms = [];
|
|
251
|
+
// Transformed to array groups
|
|
252
|
+
const groups = [];
|
|
253
|
+
const subforms = self.subforms;
|
|
254
|
+
const otherGroup = {
|
|
255
|
+
name: 'ungrouped',
|
|
256
|
+
label: 'apostrophe:ungrouped'
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Transform to array groups
|
|
260
|
+
for (const [ name, group ] of Object.entries(self.options.groups || {})) {
|
|
261
|
+
groups.push({
|
|
262
|
+
name,
|
|
263
|
+
label: group.label || name[0].toUpperCase() + name.slice(1),
|
|
264
|
+
subforms: group.subforms || []
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Push and Sort subfields to the newSubforms, add group to every subform.
|
|
268
|
+
for (const group of groups) {
|
|
269
|
+
if (!group.subforms.length) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
group.subforms.forEach(name => {
|
|
273
|
+
const subform = subforms.find(subform => subform.name === name);
|
|
274
|
+
if (subform) {
|
|
275
|
+
newSubforms.push({
|
|
276
|
+
...subform,
|
|
277
|
+
group: {
|
|
278
|
+
name: group.name,
|
|
279
|
+
label: group.label
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// Push the leftover to ungrouped. It shouldn't be possible though.
|
|
286
|
+
const leftover = subforms
|
|
287
|
+
.filter(subform =>
|
|
288
|
+
!newSubforms.some(newSubform => newSubform.name === subform.name)
|
|
289
|
+
);
|
|
290
|
+
for (const subform of leftover) {
|
|
291
|
+
newSubforms.push({
|
|
292
|
+
...subform,
|
|
293
|
+
group: otherGroup
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
self.subforms = newSubforms;
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
// Get the subset of the user schema that is relevant to the configured
|
|
300
|
+
// subforms.
|
|
301
|
+
inferUserSchema() {
|
|
302
|
+
const subforms = self.options.subforms;
|
|
303
|
+
const userSchema = self.apos.user.schema;
|
|
304
|
+
const allSettingsFields = [
|
|
305
|
+
...new Set(
|
|
306
|
+
Object.keys(subforms)
|
|
307
|
+
.reduce((acc, subform) => {
|
|
308
|
+
// Do not allow password field alongside other fields in a subform
|
|
309
|
+
if (subforms[subform].fields.includes('password')) {
|
|
310
|
+
subforms[subform].fields = [ 'password' ];
|
|
311
|
+
}
|
|
312
|
+
return acc.concat(subforms[subform].fields || []);
|
|
313
|
+
}, [])
|
|
314
|
+
)
|
|
315
|
+
];
|
|
316
|
+
self.validateSettingsSchema(allSettingsFields, userSchema);
|
|
317
|
+
|
|
318
|
+
return allSettingsFields
|
|
319
|
+
.filter(field => {
|
|
320
|
+
return userSchema.some(userField => userField.name === field);
|
|
321
|
+
})
|
|
322
|
+
// extra safety
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
.map(field => {
|
|
325
|
+
return klona(userSchema.find(userField => userField.name === field));
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
// Validate that the fields configured in the settings module exist in the
|
|
330
|
+
// user schema and are not forbidden.
|
|
331
|
+
validateSettingsSchema(settingsFieldNames, userSchema) {
|
|
332
|
+
for (const name of settingsFieldNames) {
|
|
333
|
+
if (!userSchema.some(field => field.name === name)) {
|
|
334
|
+
throw new Error(`[@apostrophecms/settings] The field "${name}" is not a valid user field.`);
|
|
335
|
+
}
|
|
336
|
+
if (self.systemForbiddenFields.includes(name)) {
|
|
337
|
+
throw new Error(`[@apostrophecms/settings] The field "${name}" is forbidden.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// Enhance the subforms - `protection` security.
|
|
343
|
+
// This method requires initialized self.subforms.
|
|
344
|
+
enhanceSubforms() {
|
|
345
|
+
// 1. Add protection flag to subforms for system protected fields.
|
|
346
|
+
for (const [ fieldName, protectionType ] of Object.entries(self.systemProtectedFields)) {
|
|
347
|
+
self.subforms = self.subforms.map(subform => {
|
|
348
|
+
if (subform.fields.includes(fieldName)) {
|
|
349
|
+
subform.protection = protectionType || true;
|
|
350
|
+
}
|
|
351
|
+
return subform;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 2. Ehhance the protected forms schema.
|
|
356
|
+
self.subforms = self.subforms.map(subform => {
|
|
357
|
+
if (!subform.protection) {
|
|
358
|
+
return subform;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 2.1. Special case for the change password subform
|
|
362
|
+
const passwordField = subform.schema.find(field => field.name === 'password');
|
|
363
|
+
if (passwordField) {
|
|
364
|
+
self.enhancePasswordSubform(passwordField, subform);
|
|
365
|
+
return subform;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 2.2. General case for all other protected subforms
|
|
369
|
+
self.enhanceProtectedSubform(subform);
|
|
370
|
+
return subform;
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
// Auto-generate and replace the subform schema for the "password change"
|
|
375
|
+
// scenario.
|
|
376
|
+
enhancePasswordSubform(passwordField, subform) {
|
|
377
|
+
const templateField = self.getPasswordTemplateField();
|
|
378
|
+
subform.help = subform.help || 'apostrophe:passwordChangeHelp';
|
|
379
|
+
if (!subform.label) {
|
|
380
|
+
subform.label = 'apostrophe:password';
|
|
381
|
+
}
|
|
382
|
+
subform.schema = [];
|
|
383
|
+
// Indicates the edge case of password change form
|
|
384
|
+
subform._passwordChangeForm = true;
|
|
385
|
+
subform.schema.push({
|
|
386
|
+
...passwordField,
|
|
387
|
+
label: 'apostrophe:passwordNew',
|
|
388
|
+
required: true
|
|
389
|
+
});
|
|
390
|
+
subform.schema.push({
|
|
391
|
+
...templateField,
|
|
392
|
+
label: 'apostrophe:passwordRepeat',
|
|
393
|
+
name: 'passwordRepeat',
|
|
394
|
+
required: true
|
|
395
|
+
});
|
|
396
|
+
subform.schema.push({
|
|
397
|
+
...templateField,
|
|
398
|
+
label: 'apostrophe:passwordCurrent',
|
|
399
|
+
name: 'passwordCurrent',
|
|
400
|
+
required: true
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// Enhance the protected subform schema based on the protection type.
|
|
405
|
+
enhanceProtectedSubform(subform) {
|
|
406
|
+
switch (subform.protection) {
|
|
407
|
+
case self.protectionTypes.password: {
|
|
408
|
+
// Last field so that it doesn't mess up with the "first field label"
|
|
409
|
+
// detection on the client side (when form label is not specified).
|
|
410
|
+
subform.schema.push({
|
|
411
|
+
...self.getPasswordTemplateField(),
|
|
412
|
+
label: 'apostrophe:passwordCurrent',
|
|
413
|
+
name: 'passwordCurrent',
|
|
414
|
+
required: true
|
|
415
|
+
});
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
// TODO `self.protectionTypes.email' in phase 3
|
|
419
|
+
|
|
420
|
+
default: {
|
|
421
|
+
throw new Error(`[@apostrophecms/settings] Not supported protection type "${subform.protection}".`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
// Clone the password field from the user schema to be used as a template
|
|
427
|
+
// for auto generated subform schema.
|
|
428
|
+
getPasswordTemplateField() {
|
|
429
|
+
const templateField = klona(self.apos.user.schema.find(field => field.name === 'password'));
|
|
430
|
+
delete templateField.moduleName;
|
|
431
|
+
delete templateField.group;
|
|
432
|
+
delete templateField.name;
|
|
433
|
+
delete templateField.label;
|
|
434
|
+
return templateField;
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
// Get subform fields by subform name.
|
|
438
|
+
// This method requires initialized self.subforms.
|
|
439
|
+
getSubformSchema(name) {
|
|
440
|
+
const subform = self.options.subforms[name];
|
|
441
|
+
if (!subform) {
|
|
442
|
+
throw new Error('notfound', `[@apostrophecms/settings] Subform "${name}" not found.`);
|
|
443
|
+
}
|
|
444
|
+
return subform.fields.map(fieldName => {
|
|
445
|
+
return self.userSchema.find(field => field.name === fieldName);
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
getSubform(name) {
|
|
450
|
+
return self.subforms.find(subform => subform.name === name);
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
// Detect protected subforms and handle them.
|
|
454
|
+
handleProtectedSubform(req, subform, payload) {
|
|
455
|
+
if (!subform.protection) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (subform._passwordChangeForm) {
|
|
459
|
+
return self.handlePasswordChangeSubform(req, subform, payload);
|
|
460
|
+
}
|
|
461
|
+
switch (subform.protection) {
|
|
462
|
+
case self.protectionTypes.password: {
|
|
463
|
+
return self.handlePasswordProtectedSubform(req, subform, payload);
|
|
464
|
+
}
|
|
465
|
+
// TODO `self.protectionTypes.email' in phase 3
|
|
466
|
+
|
|
467
|
+
// Should not happen as we validate the protected type in the init phase.
|
|
468
|
+
default: {
|
|
469
|
+
throw self.apos.error('invalid', `Not supported protected type "${subform.protection}".`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
// Handle the password change subform.
|
|
475
|
+
handlePasswordChangeSubform(req, subform, payload) {
|
|
476
|
+
const { password, passwordRepeat } = payload;
|
|
477
|
+
if (!password || passwordRepeat !== password) {
|
|
478
|
+
const invalid = self.apos.error('invalid', {
|
|
479
|
+
errors: 'invalid'
|
|
480
|
+
});
|
|
481
|
+
invalid.path = 'passwordRepeat';
|
|
482
|
+
throw [ invalid ];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return self.handlePasswordProtectedSubform(req, subform, payload);
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
// Handle the password protected subform.
|
|
489
|
+
async handlePasswordProtectedSubform(req, subform, payload) {
|
|
490
|
+
try {
|
|
491
|
+
await self.apos.user.verifyPassword(req.user, payload.passwordCurrent);
|
|
492
|
+
} catch (e) {
|
|
493
|
+
throw self.apos.error(
|
|
494
|
+
'forbidden',
|
|
495
|
+
'apostrophe:passwordCurrentError',
|
|
496
|
+
{
|
|
497
|
+
path: 'passwordCurrent'
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return subform;
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
// Handle the after save logic. If the saved subform requires reload
|
|
505
|
+
// after save, we will add session indicator that will allow the client
|
|
506
|
+
// to restore its state. The client is responsible for the actual reload.
|
|
507
|
+
// The session value contains the current subform name. The value is sent
|
|
508
|
+
// once via the `getBrowserData` method and then removed from the session.
|
|
509
|
+
handleAfterSave(req, subform) {
|
|
510
|
+
if (!subform.reload) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
req.session.aposSettingsReload = subform.name;
|
|
514
|
+
// TODO email(s) in phase 3
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
addToAdminBar() {
|
|
518
|
+
if (!self.hasSchema()) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
self.apos.adminBar.add(
|
|
522
|
+
`${self.__meta.name}:settings`,
|
|
523
|
+
'apostrophe:settings',
|
|
524
|
+
false,
|
|
525
|
+
{
|
|
526
|
+
user: true
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
addSettingsModal() {
|
|
532
|
+
if (!self.hasSchema()) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
self.apos.modal.add(
|
|
536
|
+
`${self.__meta.name}:settings`,
|
|
537
|
+
self.getComponentName('settingsModal', 'AposSettingsManager'),
|
|
538
|
+
{ moduleName: self.__meta.name }
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
getBrowserData(req) {
|
|
543
|
+
const restore = req.session.aposSettingsReload;
|
|
544
|
+
delete req.session.aposSettingsReload;
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
subforms: self.subforms,
|
|
548
|
+
action: self.action,
|
|
549
|
+
restore
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
restApiRoutes(self) {
|
|
556
|
+
return {
|
|
557
|
+
async getAll(req) {
|
|
558
|
+
if (!self.hasSchema() || !req.user) {
|
|
559
|
+
throw self.apos.error('notfound');
|
|
560
|
+
}
|
|
561
|
+
const user = await self.apos.user
|
|
562
|
+
.find(req, { _id: req.user._id })
|
|
563
|
+
.permission(false)
|
|
564
|
+
.toObject();
|
|
565
|
+
|
|
566
|
+
if (!user) {
|
|
567
|
+
throw self.apos.error('notfound');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const values = {
|
|
571
|
+
_id: user._id
|
|
572
|
+
};
|
|
573
|
+
for (const field of self.userSchema) {
|
|
574
|
+
values[field.name] = user[field.name];
|
|
575
|
+
}
|
|
576
|
+
return values;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
apiRoutes(self) {
|
|
582
|
+
return {
|
|
583
|
+
patch: {
|
|
584
|
+
':subform': async (req) => {
|
|
585
|
+
if (!self.hasSchema() || !req.user) {
|
|
586
|
+
throw self.apos.error('notfound');
|
|
587
|
+
}
|
|
588
|
+
let subform = self.getSubform(
|
|
589
|
+
self.apos.launder.string(req.params.subform)
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
if (!subform || !subform.schema.length) {
|
|
593
|
+
throw self.apos.error('notfound');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
await self.handleProtectedSubform(req, subform, req.body);
|
|
597
|
+
// Remove the auto-generated fields from the schema
|
|
598
|
+
subform = klona(subform);
|
|
599
|
+
subform.schema = subform.schema
|
|
600
|
+
.filter(field => self.userSchema.some(userField => userField.name === field.name));
|
|
601
|
+
|
|
602
|
+
const user = await self.apos.user
|
|
603
|
+
.find(req, { _id: req.user._id })
|
|
604
|
+
.permission(false)
|
|
605
|
+
.toObject();
|
|
606
|
+
|
|
607
|
+
if (!user) {
|
|
608
|
+
throw self.apos.error('notfound');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
await self.apos.schema.convert(req, subform.schema, req.body, user);
|
|
612
|
+
await self.apos.user.update(req, user, { permissions: false });
|
|
613
|
+
|
|
614
|
+
await self.handleAfterSave(req, subform, user);
|
|
615
|
+
|
|
616
|
+
const values = {
|
|
617
|
+
_id: user._id
|
|
618
|
+
};
|
|
619
|
+
for (const field of subform.schema) {
|
|
620
|
+
values[field.name] = user[field.name];
|
|
621
|
+
}
|
|
622
|
+
return values;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
};
|