apostrophe 3.48.0 → 3.50.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 +55 -2
- package/index.js +20 -2
- package/lib/locales.js +1 -1
- package/lib/moog-require.js +3 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +12 -2
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +7 -24
- package/modules/@apostrophecms/asset/index.js +27 -2
- package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +23 -2
- package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +26 -2
- package/modules/@apostrophecms/doc/index.js +149 -0
- package/modules/@apostrophecms/doc-type/index.js +9 -1
- package/modules/@apostrophecms/global/index.js +4 -15
- package/modules/@apostrophecms/i18n/i18n/en.json +3 -2
- package/modules/@apostrophecms/i18n/index.js +76 -61
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +14 -1
- package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +3 -60
- package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +3 -231
- package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +3 -96
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +2 -99
- package/modules/@apostrophecms/login/ui/apos/logic/AposForgotPasswordForm.js +68 -0
- package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +239 -0
- package/modules/@apostrophecms/login/ui/apos/logic/AposResetPasswordForm.js +105 -0
- package/modules/@apostrophecms/login/ui/apos/logic/TheAposLogin.js +107 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +9 -3
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue +1 -0
- package/modules/@apostrophecms/page/index.js +124 -1
- package/modules/@apostrophecms/piece-type/index.js +57 -9
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +11 -8
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +226 -72
- package/modules/@apostrophecms/schema/index.js +0 -1
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +35 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +21 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +12 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +178 -20
- package/modules/@apostrophecms/ui/ui/apos/components/AposFilterMenu.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -6
- package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +1 -0
- package/modules/@apostrophecms/util/index.js +5 -6
- package/modules/@apostrophecms/util/ui/src/http.js +6 -3
- package/package.json +20 -3
- package/test/change-doc-ids.js +134 -0
- package/test/i18n.js +310 -0
- package/test/static-i18n.js +0 -105
|
@@ -58,108 +58,11 @@
|
|
|
58
58
|
</template>
|
|
59
59
|
|
|
60
60
|
<script>
|
|
61
|
-
import
|
|
62
|
-
|
|
63
|
-
const STAGES = [
|
|
64
|
-
'login',
|
|
65
|
-
'forgotPassword',
|
|
66
|
-
'resetPassword'
|
|
67
|
-
];
|
|
61
|
+
import TheAposLoginLogic from 'Modules/@apostrophecms/login/logic/TheAposLogin';
|
|
68
62
|
|
|
69
63
|
export default {
|
|
70
64
|
name: 'TheAposLogin',
|
|
71
|
-
mixins: [
|
|
72
|
-
data() {
|
|
73
|
-
return {
|
|
74
|
-
stage: STAGES[0],
|
|
75
|
-
mounted: false,
|
|
76
|
-
beforeCreateFinished: false,
|
|
77
|
-
error: '',
|
|
78
|
-
passwordResetData: {},
|
|
79
|
-
context: {}
|
|
80
|
-
};
|
|
81
|
-
},
|
|
82
|
-
computed: {
|
|
83
|
-
loaded() {
|
|
84
|
-
return this.mounted && this.beforeCreateFinished;
|
|
85
|
-
},
|
|
86
|
-
showNav() {
|
|
87
|
-
return this.stage !== STAGES[0];
|
|
88
|
-
},
|
|
89
|
-
homeUrl() {
|
|
90
|
-
return `${apos.prefix}/`;
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
// We need it here and not in the login form because the version used in the footer.
|
|
94
|
-
// The context will be passed to every form, might be a good thing in the future.
|
|
95
|
-
async beforeCreate() {
|
|
96
|
-
const stateChange = parseInt(window.sessionStorage.getItem('aposStateChange'));
|
|
97
|
-
const seen = JSON.parse(window.sessionStorage.getItem('aposStateChangeSeen') || '{}');
|
|
98
|
-
if (!seen[window.location.href]) {
|
|
99
|
-
const lastModified = Date.parse(document.lastModified);
|
|
100
|
-
if (stateChange && lastModified && (lastModified < stateChange)) {
|
|
101
|
-
seen[window.location.href] = true;
|
|
102
|
-
window.sessionStorage.setItem('aposStateChangeSeen', JSON.stringify(seen));
|
|
103
|
-
location.reload();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
this.context = await apos.http.post(`${apos.login.action}/context`, {
|
|
109
|
-
busy: true
|
|
110
|
-
});
|
|
111
|
-
} catch (e) {
|
|
112
|
-
this.context = {};
|
|
113
|
-
this.error = e.message || 'apostrophe:loginErrorGeneric';
|
|
114
|
-
} finally {
|
|
115
|
-
this.beforeCreateFinished = true;
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
created() {
|
|
119
|
-
const url = new URL(document.location);
|
|
120
|
-
const data = {
|
|
121
|
-
email: url.searchParams.get('email'),
|
|
122
|
-
reset: url.searchParams.get('reset')
|
|
123
|
-
};
|
|
124
|
-
if (data.email && data.reset) {
|
|
125
|
-
this.passwordResetData = data;
|
|
126
|
-
this.setStage('resetPassword');
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
mounted() {
|
|
130
|
-
this.mounted = true;
|
|
131
|
-
},
|
|
132
|
-
methods: {
|
|
133
|
-
setStage(name) {
|
|
134
|
-
// 1. Enabled status per stage. A bit cryptic but effective.
|
|
135
|
-
// Search for a method composed of the `name` + `Enabled`
|
|
136
|
-
// (e.g. `forgotPasswordEnabled` and execute it (should return boolean).
|
|
137
|
-
// If no method is found it is enabled. Fallback to the default stage.
|
|
138
|
-
const enabled = this[`${name}Enabled`]?.() ?? true;
|
|
139
|
-
if (!enabled) {
|
|
140
|
-
this.stage = STAGES[0];
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// 2. Set it only if it's a known stage
|
|
144
|
-
if (STAGES.includes(name)) {
|
|
145
|
-
this.stage = name;
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
// 3. Fallback to the default stage
|
|
149
|
-
this.stage = STAGES[0];
|
|
150
|
-
},
|
|
151
|
-
forgotPasswordEnabled() {
|
|
152
|
-
return apos.login.passwordResetEnabled;
|
|
153
|
-
},
|
|
154
|
-
resetPasswordEnabled() {
|
|
155
|
-
return apos.login.passwordResetEnabled;
|
|
156
|
-
},
|
|
157
|
-
onRedirect(loc) {
|
|
158
|
-
window.sessionStorage.setItem('aposStateChange', Date.now());
|
|
159
|
-
window.sessionStorage.setItem('aposStateChangeSeen', '{}');
|
|
160
|
-
location.assign(loc);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
65
|
+
mixins: [ TheAposLoginLogic ]
|
|
163
66
|
};
|
|
164
67
|
</script>
|
|
165
68
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// This is the business logic of the AposForgotPasswordForm Vue component.
|
|
2
|
+
// It is in a separate file so that you can override the component's templates
|
|
3
|
+
// and styles just by copying the .vue file to your project, and leave the business logic
|
|
4
|
+
// unchanged.
|
|
5
|
+
|
|
6
|
+
import AposLoginFormMixin from 'Modules/@apostrophecms/login/mixins/AposLoginFormMixin';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
mixins: [ AposLoginFormMixin ],
|
|
10
|
+
emits: [ 'set-stage' ],
|
|
11
|
+
data() {
|
|
12
|
+
return {
|
|
13
|
+
busy: false,
|
|
14
|
+
done: false,
|
|
15
|
+
schema: [
|
|
16
|
+
{
|
|
17
|
+
name: 'email',
|
|
18
|
+
label: 'apostrophe:email',
|
|
19
|
+
placeholder: 'apostrophe:loginEnterEmail',
|
|
20
|
+
type: 'string',
|
|
21
|
+
required: true
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
computed: {
|
|
27
|
+
disabled() {
|
|
28
|
+
return this.doc.hasErrors;
|
|
29
|
+
},
|
|
30
|
+
help() {
|
|
31
|
+
if (this.done) {
|
|
32
|
+
return this.$t('apostrophe:loginResetRequestDone', {
|
|
33
|
+
email: this.doc.data.email
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return this.$t('apostrophe:loginResetPasswordRequest');
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
created() {
|
|
40
|
+
if (!this.passwordResetEnabled) {
|
|
41
|
+
this.$emit('set-stage', 'login');
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
methods: {
|
|
45
|
+
async submit() {
|
|
46
|
+
if (this.busy) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.busy = true;
|
|
50
|
+
this.error = '';
|
|
51
|
+
|
|
52
|
+
await this.requestReset();
|
|
53
|
+
},
|
|
54
|
+
async requestReset() {
|
|
55
|
+
try {
|
|
56
|
+
await apos.http.post(`${apos.login.action}/reset-request`, {
|
|
57
|
+
busy: true,
|
|
58
|
+
body: { ...this.doc.data }
|
|
59
|
+
});
|
|
60
|
+
this.done = true;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
this.error = e.message || 'apostrophe:loginErrorGeneric';
|
|
63
|
+
} finally {
|
|
64
|
+
this.busy = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// This is the business logic of the AposLoginForm Vue component.
|
|
2
|
+
// It is in a separate file so that you can override the component's templates
|
|
3
|
+
// and styles just by copying the .vue file to your project, and leave the business logic
|
|
4
|
+
// unchanged.
|
|
5
|
+
|
|
6
|
+
import AposLoginFormMixin from 'Modules/@apostrophecms/login/mixins/AposLoginFormMixin';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
mixins: [ AposLoginFormMixin ],
|
|
10
|
+
emits: [ 'redirect', 'set-stage' ],
|
|
11
|
+
data() {
|
|
12
|
+
return {
|
|
13
|
+
phase: 'beforeSubmit',
|
|
14
|
+
busy: false,
|
|
15
|
+
schema: [
|
|
16
|
+
{
|
|
17
|
+
name: 'username',
|
|
18
|
+
label: 'Username',
|
|
19
|
+
placeholder: 'Enter username',
|
|
20
|
+
type: 'string',
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'password',
|
|
25
|
+
label: 'Password',
|
|
26
|
+
placeholder: 'Enter password',
|
|
27
|
+
type: 'password',
|
|
28
|
+
required: true
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
requirements: getRequirements(),
|
|
32
|
+
requirementProps: {},
|
|
33
|
+
fetchingRequirementProps: false
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
computed: {
|
|
37
|
+
disabled() {
|
|
38
|
+
return this.doc.hasErrors ||
|
|
39
|
+
!!this.beforeSubmitRequirements.find(requirement => !requirement.done);
|
|
40
|
+
},
|
|
41
|
+
beforeSubmitRequirements() {
|
|
42
|
+
return this.requirements.filter(requirement => requirement.phase === 'beforeSubmit');
|
|
43
|
+
},
|
|
44
|
+
// The currently active requirement expecting a solo presentation.
|
|
45
|
+
// Currently it only concerns `afterPasswordVerified` requirements.
|
|
46
|
+
// beforeSubmit requirements are not presented solo.
|
|
47
|
+
activeSoloRequirement() {
|
|
48
|
+
return (this.phase === 'afterPasswordVerified') &&
|
|
49
|
+
this.requirements.find(requirement =>
|
|
50
|
+
(requirement.phase === 'afterPasswordVerified') && !requirement.done
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
watch: {
|
|
55
|
+
context(newVal) {
|
|
56
|
+
this.requirementProps = newVal.requirementProps;
|
|
57
|
+
},
|
|
58
|
+
async activeSoloRequirement(newVal) {
|
|
59
|
+
if (
|
|
60
|
+
(this.phase === 'afterPasswordVerified') &&
|
|
61
|
+
(newVal?.phase === 'afterPasswordVerified') &&
|
|
62
|
+
newVal.propsRequired &&
|
|
63
|
+
!(newVal.success || newVal.error)
|
|
64
|
+
) {
|
|
65
|
+
try {
|
|
66
|
+
this.fetchingRequirementProps = true;
|
|
67
|
+
const data = await apos.http.post(`${apos.login.action}/requirement-props`, {
|
|
68
|
+
busy: true,
|
|
69
|
+
body: {
|
|
70
|
+
name: newVal.name,
|
|
71
|
+
incompleteToken: this.incompleteToken
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
this.requirementProps = {
|
|
75
|
+
...this.requirementProps,
|
|
76
|
+
[newVal.name]: data
|
|
77
|
+
};
|
|
78
|
+
} catch (e) {
|
|
79
|
+
this.error = e.message || 'apostrophe:loginErrorGeneric';
|
|
80
|
+
} finally {
|
|
81
|
+
this.fetchingRequirementProps = false;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
created() {
|
|
89
|
+
this.requirementProps = this.context.requirementProps;
|
|
90
|
+
},
|
|
91
|
+
methods: {
|
|
92
|
+
async submit() {
|
|
93
|
+
if (this.busy) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.busy = true;
|
|
97
|
+
this.error = '';
|
|
98
|
+
|
|
99
|
+
await this.invokeInitialLoginApi();
|
|
100
|
+
},
|
|
101
|
+
async invokeInitialLoginApi() {
|
|
102
|
+
try {
|
|
103
|
+
const response = await apos.http.post(`${apos.login.action}/login`, {
|
|
104
|
+
busy: true,
|
|
105
|
+
body: {
|
|
106
|
+
...this.doc.data,
|
|
107
|
+
requirements: this.getInitialSubmitRequirementsData(),
|
|
108
|
+
session: true
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (response && response.incompleteToken) {
|
|
112
|
+
this.incompleteToken = response.incompleteToken;
|
|
113
|
+
this.phase = 'afterPasswordVerified';
|
|
114
|
+
} else {
|
|
115
|
+
this.redirectAfterLogin();
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
this.error = e.message || 'An error occurred. Please try again.';
|
|
119
|
+
this.phase = 'beforeSubmit';
|
|
120
|
+
} finally {
|
|
121
|
+
this.busy = false;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
getInitialSubmitRequirementsData() {
|
|
125
|
+
return Object.fromEntries(this.requirements
|
|
126
|
+
.filter(r => r.phase !== 'afterPasswordVerified' || !r.done)
|
|
127
|
+
.map(r => ([
|
|
128
|
+
r.name,
|
|
129
|
+
r.value
|
|
130
|
+
])));
|
|
131
|
+
},
|
|
132
|
+
async invokeFinalLoginApi() {
|
|
133
|
+
try {
|
|
134
|
+
await apos.http.post(`${apos.login.action}/login`, {
|
|
135
|
+
busy: true,
|
|
136
|
+
body: {
|
|
137
|
+
...this.doc.data,
|
|
138
|
+
incompleteToken: this.incompleteToken,
|
|
139
|
+
requirements: this.getFinalSubmitRequirementsData(),
|
|
140
|
+
session: true
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
this.redirectAfterLogin();
|
|
144
|
+
} catch (e) {
|
|
145
|
+
this.error = e.message || 'An error occurred. Please try again.';
|
|
146
|
+
this.phase = 'beforeSubmit';
|
|
147
|
+
} finally {
|
|
148
|
+
this.busy = false;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
getFinalSubmitRequirementsData() {
|
|
152
|
+
return Object.fromEntries(this.requirements.filter(r => r.phase === 'afterPasswordVerified').map(r => ([
|
|
153
|
+
r.name,
|
|
154
|
+
r.value
|
|
155
|
+
])));
|
|
156
|
+
},
|
|
157
|
+
redirectAfterLogin() {
|
|
158
|
+
// TODO handle situation where user should be sent somewhere other than homepage.
|
|
159
|
+
// Redisplay homepage with editing interface
|
|
160
|
+
this.$emit('redirect', `${apos.prefix}/`);
|
|
161
|
+
},
|
|
162
|
+
async requirementBlock(requirementBlock) {
|
|
163
|
+
const requirement = this.requirements
|
|
164
|
+
.find(requirement => requirement.name === requirementBlock.name);
|
|
165
|
+
requirement.done = false;
|
|
166
|
+
requirement.value = undefined;
|
|
167
|
+
},
|
|
168
|
+
async requirementDone(requirementDone, value) {
|
|
169
|
+
const requirement = this.requirements
|
|
170
|
+
.find(requirement => requirement.name === requirementDone.name);
|
|
171
|
+
|
|
172
|
+
if (requirement.phase === 'beforeSubmit') {
|
|
173
|
+
requirement.done = true;
|
|
174
|
+
requirement.value = value;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
requirement.error = null;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await apos.http.post(`${apos.login.action}/requirement-verify`, {
|
|
182
|
+
busy: true,
|
|
183
|
+
body: {
|
|
184
|
+
name: requirement.name,
|
|
185
|
+
value,
|
|
186
|
+
incompleteToken: this.incompleteToken
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
requirement.success = true;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
requirement.error = err;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Avoids the need for a deep watch
|
|
196
|
+
this.requirements = [ ...this.requirements ];
|
|
197
|
+
|
|
198
|
+
if (requirement.success && !requirement.askForConfirmation) {
|
|
199
|
+
requirement.done = true;
|
|
200
|
+
|
|
201
|
+
if (!this.activeSoloRequirement) {
|
|
202
|
+
await this.invokeFinalLoginApi();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async requirementConfirmed (requirementConfirmed) {
|
|
208
|
+
const requirement = this.requirements
|
|
209
|
+
.find(requirement => requirement.name === requirementConfirmed.name);
|
|
210
|
+
|
|
211
|
+
requirement.done = true;
|
|
212
|
+
|
|
213
|
+
if (!this.activeSoloRequirement) {
|
|
214
|
+
await this.invokeFinalLoginApi();
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
getRequirementProps(name) {
|
|
218
|
+
return this.requirementProps[name] || {};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
function getRequirements() {
|
|
224
|
+
const requirements = Object.entries(apos.login.requirements).map(([ name, requirement ]) => {
|
|
225
|
+
return {
|
|
226
|
+
name,
|
|
227
|
+
component: requirement.component || name,
|
|
228
|
+
...requirement,
|
|
229
|
+
done: false,
|
|
230
|
+
value: null,
|
|
231
|
+
success: null,
|
|
232
|
+
error: null
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
return [
|
|
236
|
+
...requirements.filter(r => r.phase === 'beforeSubmit'),
|
|
237
|
+
...requirements.filter(r => r.phase === 'afterPasswordVerified')
|
|
238
|
+
];
|
|
239
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// This is the business logic of the AposResetPasswordForm Vue component.
|
|
2
|
+
// It is in a separate file so that you can override the component's templates
|
|
3
|
+
// and styles just by copying the .vue file to your project, and leave the business logic
|
|
4
|
+
// unchanged.
|
|
5
|
+
|
|
6
|
+
import AposLoginFormMixin from 'Modules/@apostrophecms/login/mixins/AposLoginFormMixin';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: 'AposResetPasswordForm',
|
|
10
|
+
mixins: [ AposLoginFormMixin ],
|
|
11
|
+
props: {
|
|
12
|
+
data: {
|
|
13
|
+
type: Object,
|
|
14
|
+
default: function() {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
emits: [ 'set-stage' ],
|
|
20
|
+
data() {
|
|
21
|
+
return {
|
|
22
|
+
ready: false,
|
|
23
|
+
busy: false,
|
|
24
|
+
valid: true,
|
|
25
|
+
done: false,
|
|
26
|
+
contextErrorReceived: false,
|
|
27
|
+
schema: [
|
|
28
|
+
{
|
|
29
|
+
name: 'password',
|
|
30
|
+
label: 'apostrophe:newPassword',
|
|
31
|
+
type: 'password',
|
|
32
|
+
required: true
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
computed: {
|
|
38
|
+
disabled() {
|
|
39
|
+
return this.doc.hasErrors;
|
|
40
|
+
},
|
|
41
|
+
help() {
|
|
42
|
+
if (!this.valid) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
if (this.done) {
|
|
46
|
+
return this.error ? '' : this.$t('apostrophe:loginResetDone');
|
|
47
|
+
}
|
|
48
|
+
return this.$t('apostrophe:loginResetInfo', {
|
|
49
|
+
email: this.data.email
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async created() {
|
|
54
|
+
if (!this.passwordResetEnabled) {
|
|
55
|
+
this.$emit('set-stage', 'login');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.busy = true;
|
|
59
|
+
await this.verify();
|
|
60
|
+
this.ready = true;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
methods: {
|
|
64
|
+
async submit() {
|
|
65
|
+
if (this.busy) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.busy = true;
|
|
69
|
+
this.error = '';
|
|
70
|
+
|
|
71
|
+
await this.reset();
|
|
72
|
+
},
|
|
73
|
+
async verify() {
|
|
74
|
+
try {
|
|
75
|
+
await apos.http.get(`${apos.login.action}/reset`, {
|
|
76
|
+
busy: true,
|
|
77
|
+
qs: { ...this.data }
|
|
78
|
+
});
|
|
79
|
+
this.valid = true;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
this.valid = false;
|
|
82
|
+
this.done = true;
|
|
83
|
+
this.error = e.message || 'apostrophe:loginErrorGeneric';
|
|
84
|
+
} finally {
|
|
85
|
+
this.busy = false;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async reset() {
|
|
89
|
+
try {
|
|
90
|
+
await apos.http.post(`${apos.login.action}/reset`, {
|
|
91
|
+
busy: true,
|
|
92
|
+
body: {
|
|
93
|
+
...this.data,
|
|
94
|
+
...this.doc.data
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.done = true;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
this.error = e.message || 'apostrophe:loginErrorGeneric';
|
|
100
|
+
} finally {
|
|
101
|
+
this.busy = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// This is the business logic of the TheAposLogin Vue component.
|
|
2
|
+
// It is in a separate file so that you can override the component's templates
|
|
3
|
+
// and styles just by copying the .vue file to your project, and leave the business logic
|
|
4
|
+
// unchanged.
|
|
5
|
+
|
|
6
|
+
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
|
|
7
|
+
|
|
8
|
+
const STAGES = [
|
|
9
|
+
'login',
|
|
10
|
+
'forgotPassword',
|
|
11
|
+
'resetPassword'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
mixins: [ AposThemeMixin ],
|
|
16
|
+
data() {
|
|
17
|
+
return {
|
|
18
|
+
stage: STAGES[0],
|
|
19
|
+
mounted: false,
|
|
20
|
+
beforeCreateFinished: false,
|
|
21
|
+
error: '',
|
|
22
|
+
passwordResetData: {},
|
|
23
|
+
context: {}
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
computed: {
|
|
27
|
+
loaded() {
|
|
28
|
+
return this.mounted && this.beforeCreateFinished;
|
|
29
|
+
},
|
|
30
|
+
showNav() {
|
|
31
|
+
return this.stage !== STAGES[0];
|
|
32
|
+
},
|
|
33
|
+
homeUrl() {
|
|
34
|
+
return `${apos.prefix}/`;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
// We need it here and not in the login form because the version used in the footer.
|
|
38
|
+
// The context will be passed to every form, might be a good thing in the future.
|
|
39
|
+
async beforeCreate() {
|
|
40
|
+
const stateChange = parseInt(window.sessionStorage.getItem('aposStateChange'));
|
|
41
|
+
const seen = JSON.parse(window.sessionStorage.getItem('aposStateChangeSeen') || '{}');
|
|
42
|
+
if (!seen[window.location.href]) {
|
|
43
|
+
const lastModified = Date.parse(document.lastModified);
|
|
44
|
+
if (stateChange && lastModified && (lastModified < stateChange)) {
|
|
45
|
+
seen[window.location.href] = true;
|
|
46
|
+
window.sessionStorage.setItem('aposStateChangeSeen', JSON.stringify(seen));
|
|
47
|
+
location.reload();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
this.context = await apos.http.post(`${apos.login.action}/context`, {
|
|
53
|
+
busy: true
|
|
54
|
+
});
|
|
55
|
+
} catch (e) {
|
|
56
|
+
this.context = {};
|
|
57
|
+
this.error = e.message || 'apostrophe:loginErrorGeneric';
|
|
58
|
+
} finally {
|
|
59
|
+
this.beforeCreateFinished = true;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
created() {
|
|
63
|
+
const url = new URL(document.location);
|
|
64
|
+
const data = {
|
|
65
|
+
email: url.searchParams.get('email'),
|
|
66
|
+
reset: url.searchParams.get('reset')
|
|
67
|
+
};
|
|
68
|
+
if (data.email && data.reset) {
|
|
69
|
+
this.passwordResetData = data;
|
|
70
|
+
this.setStage('resetPassword');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
mounted() {
|
|
74
|
+
this.mounted = true;
|
|
75
|
+
},
|
|
76
|
+
methods: {
|
|
77
|
+
setStage(name) {
|
|
78
|
+
// 1. Enabled status per stage. A bit cryptic but effective.
|
|
79
|
+
// Search for a method composed of the `name` + `Enabled`
|
|
80
|
+
// (e.g. `forgotPasswordEnabled` and execute it (should return boolean).
|
|
81
|
+
// If no method is found it is enabled. Fallback to the default stage.
|
|
82
|
+
const enabled = this[`${name}Enabled`]?.() ?? true;
|
|
83
|
+
if (!enabled) {
|
|
84
|
+
this.stage = STAGES[0];
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// 2. Set it only if it's a known stage
|
|
88
|
+
if (STAGES.includes(name)) {
|
|
89
|
+
this.stage = name;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// 3. Fallback to the default stage
|
|
93
|
+
this.stage = STAGES[0];
|
|
94
|
+
},
|
|
95
|
+
forgotPasswordEnabled() {
|
|
96
|
+
return apos.login.passwordResetEnabled;
|
|
97
|
+
},
|
|
98
|
+
resetPasswordEnabled() {
|
|
99
|
+
return apos.login.passwordResetEnabled;
|
|
100
|
+
},
|
|
101
|
+
onRedirect(loc) {
|
|
102
|
+
window.sessionStorage.setItem('aposStateChange', Date.now());
|
|
103
|
+
window.sessionStorage.setItem('aposStateChangeSeen', '{}');
|
|
104
|
+
location.assign(loc);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
v-if="canSelectAll"
|
|
6
6
|
label="apostrophe:select"
|
|
7
7
|
type="outline"
|
|
8
|
+
:modifiers="['small']"
|
|
8
9
|
text-color="var(--a-base-1)"
|
|
9
10
|
:icon-only="true"
|
|
10
11
|
:icon="checkboxIcon"
|
|
@@ -24,9 +25,9 @@
|
|
|
24
25
|
<AposButton
|
|
25
26
|
v-if="!operations"
|
|
26
27
|
:label="label"
|
|
27
|
-
:icon-only="true"
|
|
28
28
|
:icon="icon"
|
|
29
29
|
:disabled="!checkedCount"
|
|
30
|
+
:modifiers="['small']"
|
|
30
31
|
type="outline"
|
|
31
32
|
@click="confirmOperation({ action, label, ...rest })"
|
|
32
33
|
/>
|
|
@@ -302,7 +303,12 @@ export default {
|
|
|
302
303
|
</script>
|
|
303
304
|
|
|
304
305
|
<style lang="scss" scoped>
|
|
305
|
-
.apos-manager-toolbar ::v-deep
|
|
306
|
-
|
|
306
|
+
.apos-manager-toolbar ::v-deep {
|
|
307
|
+
.apos-field--search {
|
|
308
|
+
width: 250px;
|
|
309
|
+
}
|
|
310
|
+
.apos-input {
|
|
311
|
+
height: 32px;
|
|
312
|
+
}
|
|
307
313
|
}
|
|
308
314
|
</style>
|