@adminforth/open-signup 1.0.3 → 1.0.4

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.
@@ -37,26 +37,28 @@
37
37
  <!-- Modal body -->
38
38
  <div class="p-4 md:p-5">
39
39
  <form v-if="!requestSent" class="space-y-4" role="alert" @submit.prevent>
40
- <div class="relative">
40
+ <div v-if="!verifyToken" class="relative">
41
41
  <label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
42
42
  <input type="email" name="email" id="email"
43
43
  tabindex="1"
44
- @keydown.enter="passwordInput.focus()"
44
+ autocomplete="username"
45
+ @keydown.enter="passwordInput?.focus()"
45
46
  ref="emailInput"
46
47
  class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
47
48
  placeholder="name@company.com"
48
49
  required
49
50
  />
50
51
  </div>
51
- <div class="relative">
52
+ <div v-if="isPasswordNeeded" class="relative">
52
53
  <label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New password</label>
53
54
  <input
54
55
  tabindex="2"
56
+ autocomplete="new-password"
55
57
  ref="passwordInput"
56
58
  :type="unmasked ? 'text' : 'password'"
57
59
  name="password" id="password"
58
60
  v-model="password"
59
- @keydown.enter="passwordConfirmationInput.focus()"
61
+ @keydown.enter="passwordConfirmationInput?.focus()"
60
62
  class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
61
63
  placeholder="New password" required
62
64
  />
@@ -71,10 +73,11 @@
71
73
  </button>
72
74
  </div>
73
75
 
74
- <div class="relative">
76
+ <div v-if="isPasswordNeeded" class="relative">
75
77
  <label for="password_confirmation" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm new password</label>
76
78
  <input
77
79
  ref="passwordConfirmationInput"
80
+ autocomplete="new-password"
78
81
  :type="unmasked ? 'text' : 'password'"
79
82
  name="password_confirmation" id="password_confirmation"
80
83
  tabindex="3"
@@ -105,7 +108,7 @@
105
108
 
106
109
  <Button
107
110
  :disabled="inProgress || (validationRunning && validationError)"
108
- @click="doSignup"
111
+ @click="() => verifyToken ? signupAfterEmailConfirmation() : doSignup()"
109
112
  type="submit"
110
113
  class="w-full"
111
114
  :loader="inProgress"
@@ -115,9 +118,11 @@
115
118
  </form>
116
119
  <!-- END of set new paasord -->
117
120
  <div v-else class="flex items center justify-center p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-green-800 dark:text-green-400" role="alert">
118
- If user with specified email exists in our db, then request was sent. Please check your email at {{ sentToEmail }} to reset your password.
119
- </div>
120
-
121
+ Please check your email at {{ sentToEmail }} to confirm your email address.
122
+ </div>
123
+ <p class="text-gray-500 dark:text-gray-400 font-sm text-right mt-3">
124
+ or <Link to="/login">{{ toLoginText }}</Link>
125
+ </p>
121
126
  </div>
122
127
  </div>
123
128
  </div>
@@ -128,7 +133,6 @@
128
133
 
129
134
 
130
135
  <script setup lang="ts">
131
-
132
136
  import { onMounted, ref, computed, Ref } from 'vue';
133
137
  import { useCoreStore } from '@/stores/core';
134
138
  import { useUserStore } from '@/stores/user';
@@ -136,27 +140,33 @@ import { callAdminForthApi, loadFile, applyRegexValidation } from '@/utils';
136
140
  import { useRoute, useRouter } from 'vue-router';
137
141
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
138
142
  import Button from '@/afcl/Button.vue';
143
+ import Link from '@/afcl/Link.vue';
139
144
 
140
145
  const inProgress = ref(false);
141
146
 
142
147
  const coreStore = useCoreStore();
143
148
  const requestSent = ref(false);
144
149
 
145
- const emailInput = ref(null);
146
- const passwordInput = ref(null);
147
- const passwordConfirmationInput = ref(null);
150
+ const emailInput: Ref<HTMLInputElement | null> = ref(null);
151
+ const passwordInput: Ref<HTMLInputElement | null> = ref(null);
152
+ const passwordConfirmationInput: Ref<HTMLInputElement | null> = ref(null);
148
153
 
149
154
  const password = ref("");
150
155
  const passwordConfirmation = ref("");
151
156
  const unmasked = ref(false);
152
157
  const sentToEmail: Ref<string> = ref('');
153
158
 
159
+ const requestEmailConfirmation = computed(() => route.meta.requestEmailConfirmation);
160
+ const verifyToken = computed(() => route.query.token);
161
+ const toLoginText = computed(() => `${verifyToken.value ? 'Go' : 'Back'} to login`);
162
+ const isPasswordNeeded = computed(() => !requestEmailConfirmation.value || (requestEmailConfirmation.value && verifyToken.value));
163
+
154
164
  const user = useUserStore();
155
165
 
156
166
  const route = useRoute();
157
167
  const router = useRouter();
158
168
 
159
- function checkPassowrd() {
169
+ function checkPassword() {
160
170
 
161
171
  if (!password.value || !passwordConfirmation.value) {
162
172
  return 'Please enter both password and password confirmation';
@@ -187,39 +197,15 @@ const validationRunning = ref(false);
187
197
 
188
198
  const validationError = computed(() => {
189
199
  if (validationRunning.value) {
190
- return checkPassowrd();
200
+ return checkPassword();
191
201
  }
192
202
  return null;
193
203
  });
194
204
 
195
- const error: Ref<str | null> = ref(null);
205
+ const error: Ref<string | null> = ref(null);
196
206
 
197
207
  onMounted(async () => {
198
- if (route.query.token) {
199
- // call method completeVerifiedSignup to confirm { token }
200
- // if success, then redirect to login page
201
-
202
- const resp = await callAdminForthApi({
203
- path: `/plugin/${route.meta.pluginInstanceId}/complete-verified-signup`,
204
- method: 'POST',
205
- body: {
206
- token: route.query.token,
207
- }
208
- });
209
- if (resp.error) {
210
- window.adminforth.alert({
211
- message: `Error fetching data: ${error.message}`,
212
- variant: 'danger',
213
- });
214
- } else if (resp.redirectTo) {
215
- router.push(resp.redirectTo);
216
- } else {
217
- await user.finishLogin();
218
- }
219
- return;
220
-
221
- }
222
- emailInput.value.focus();
208
+ emailInput.value?.focus();
223
209
  });
224
210
 
225
211
 
@@ -242,7 +228,7 @@ async function doSignup() {
242
228
  error.value = 'Please enter your email';
243
229
  return;
244
230
  }
245
- if (checkPassowrd()) {
231
+ if (!requestEmailConfirmation.value && checkPassword()) {
246
232
  validationRunning.value = true;
247
233
  return;
248
234
  }
@@ -254,7 +240,7 @@ async function doSignup() {
254
240
  body: {
255
241
  email,
256
242
  url: window.location.origin + window.location.pathname,
257
- password: password.value,
243
+ ...(!requestEmailConfirmation.value ? {password: password.value} : {}),
258
244
  }
259
245
  });
260
246
  inProgress.value = false;
@@ -264,15 +250,37 @@ async function doSignup() {
264
250
  error.value = null;
265
251
  requestSent.value = true;
266
252
  sentToEmail.value = email;
267
- if (resp.redirectTo) {
253
+ if (resp.redirectTo && !requestEmailConfirmation.value) {
268
254
  router.push(resp.redirectTo);
269
- } else {
255
+ } else if (!requestEmailConfirmation.value) {
270
256
  error.value = null;
271
257
  await user.finishLogin();
272
258
  }
273
259
  }
274
260
  }
275
261
 
276
-
277
-
262
+ const signupAfterEmailConfirmation = async () => {
263
+ if (checkPassword()) {
264
+ validationRunning.value = true;
265
+ return;
266
+ }
267
+ const resp = await callAdminForthApi({
268
+ path: `/plugin/${route.meta.pluginInstanceId}/complete-verified-signup`,
269
+ method: 'POST',
270
+ body: {
271
+ token: verifyToken.value,
272
+ password: password.value,
273
+ }
274
+ });
275
+ if (resp.error) {
276
+ window.adminforth.alert({
277
+ message: `Error fetching data: ${resp.error}`,
278
+ variant: 'danger',
279
+ });
280
+ } else if (resp.redirectTo) {
281
+ router.push(resp.redirectTo);
282
+ } else {
283
+ await user.finishLogin();
284
+ }
285
+ }
278
286
  </script>
@@ -37,26 +37,28 @@
37
37
  <!-- Modal body -->
38
38
  <div class="p-4 md:p-5">
39
39
  <form v-if="!requestSent" class="space-y-4" role="alert" @submit.prevent>
40
- <div class="relative">
40
+ <div v-if="!verifyToken" class="relative">
41
41
  <label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
42
42
  <input type="email" name="email" id="email"
43
43
  tabindex="1"
44
- @keydown.enter="passwordInput.focus()"
44
+ autocomplete="username"
45
+ @keydown.enter="passwordInput?.focus()"
45
46
  ref="emailInput"
46
47
  class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
47
48
  placeholder="name@company.com"
48
49
  required
49
50
  />
50
51
  </div>
51
- <div class="relative">
52
+ <div v-if="isPasswordNeeded" class="relative">
52
53
  <label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New password</label>
53
54
  <input
54
55
  tabindex="2"
56
+ autocomplete="new-password"
55
57
  ref="passwordInput"
56
58
  :type="unmasked ? 'text' : 'password'"
57
59
  name="password" id="password"
58
60
  v-model="password"
59
- @keydown.enter="passwordConfirmationInput.focus()"
61
+ @keydown.enter="passwordConfirmationInput?.focus()"
60
62
  class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
61
63
  placeholder="New password" required
62
64
  />
@@ -71,10 +73,11 @@
71
73
  </button>
72
74
  </div>
73
75
 
74
- <div class="relative">
76
+ <div v-if="isPasswordNeeded" class="relative">
75
77
  <label for="password_confirmation" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm new password</label>
76
78
  <input
77
79
  ref="passwordConfirmationInput"
80
+ autocomplete="new-password"
78
81
  :type="unmasked ? 'text' : 'password'"
79
82
  name="password_confirmation" id="password_confirmation"
80
83
  tabindex="3"
@@ -105,7 +108,7 @@
105
108
 
106
109
  <Button
107
110
  :disabled="inProgress || (validationRunning && validationError)"
108
- @click="doSignup"
111
+ @click="() => verifyToken ? signupAfterEmailConfirmation() : doSignup()"
109
112
  type="submit"
110
113
  class="w-full"
111
114
  :loader="inProgress"
@@ -115,9 +118,11 @@
115
118
  </form>
116
119
  <!-- END of set new paasord -->
117
120
  <div v-else class="flex items center justify-center p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-green-800 dark:text-green-400" role="alert">
118
- If user with specified email exists in our db, then request was sent. Please check your email at {{ sentToEmail }} to reset your password.
119
- </div>
120
-
121
+ Please check your email at {{ sentToEmail }} to confirm your email address.
122
+ </div>
123
+ <p class="text-gray-500 dark:text-gray-400 font-sm text-right mt-3">
124
+ or <Link to="/login">{{ toLoginText }}</Link>
125
+ </p>
121
126
  </div>
122
127
  </div>
123
128
  </div>
@@ -128,7 +133,6 @@
128
133
 
129
134
 
130
135
  <script setup lang="ts">
131
-
132
136
  import { onMounted, ref, computed, Ref } from 'vue';
133
137
  import { useCoreStore } from '@/stores/core';
134
138
  import { useUserStore } from '@/stores/user';
@@ -136,27 +140,33 @@ import { callAdminForthApi, loadFile, applyRegexValidation } from '@/utils';
136
140
  import { useRoute, useRouter } from 'vue-router';
137
141
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
138
142
  import Button from '@/afcl/Button.vue';
143
+ import Link from '@/afcl/Link.vue';
139
144
 
140
145
  const inProgress = ref(false);
141
146
 
142
147
  const coreStore = useCoreStore();
143
148
  const requestSent = ref(false);
144
149
 
145
- const emailInput = ref(null);
146
- const passwordInput = ref(null);
147
- const passwordConfirmationInput = ref(null);
150
+ const emailInput: Ref<HTMLInputElement | null> = ref(null);
151
+ const passwordInput: Ref<HTMLInputElement | null> = ref(null);
152
+ const passwordConfirmationInput: Ref<HTMLInputElement | null> = ref(null);
148
153
 
149
154
  const password = ref("");
150
155
  const passwordConfirmation = ref("");
151
156
  const unmasked = ref(false);
152
157
  const sentToEmail: Ref<string> = ref('');
153
158
 
159
+ const requestEmailConfirmation = computed(() => route.meta.requestEmailConfirmation);
160
+ const verifyToken = computed(() => route.query.token);
161
+ const toLoginText = computed(() => `${verifyToken.value ? 'Go' : 'Back'} to login`);
162
+ const isPasswordNeeded = computed(() => !requestEmailConfirmation.value || (requestEmailConfirmation.value && verifyToken.value));
163
+
154
164
  const user = useUserStore();
155
165
 
156
166
  const route = useRoute();
157
167
  const router = useRouter();
158
168
 
159
- function checkPassowrd() {
169
+ function checkPassword() {
160
170
 
161
171
  if (!password.value || !passwordConfirmation.value) {
162
172
  return 'Please enter both password and password confirmation';
@@ -187,39 +197,15 @@ const validationRunning = ref(false);
187
197
 
188
198
  const validationError = computed(() => {
189
199
  if (validationRunning.value) {
190
- return checkPassowrd();
200
+ return checkPassword();
191
201
  }
192
202
  return null;
193
203
  });
194
204
 
195
- const error: Ref<str | null> = ref(null);
205
+ const error: Ref<string | null> = ref(null);
196
206
 
197
207
  onMounted(async () => {
198
- if (route.query.token) {
199
- // call method completeVerifiedSignup to confirm { token }
200
- // if success, then redirect to login page
201
-
202
- const resp = await callAdminForthApi({
203
- path: `/plugin/${route.meta.pluginInstanceId}/complete-verified-signup`,
204
- method: 'POST',
205
- body: {
206
- token: route.query.token,
207
- }
208
- });
209
- if (resp.error) {
210
- window.adminforth.alert({
211
- message: `Error fetching data: ${error.message}`,
212
- variant: 'danger',
213
- });
214
- } else if (resp.redirectTo) {
215
- router.push(resp.redirectTo);
216
- } else {
217
- await user.finishLogin();
218
- }
219
- return;
220
-
221
- }
222
- emailInput.value.focus();
208
+ emailInput.value?.focus();
223
209
  });
224
210
 
225
211
 
@@ -242,7 +228,7 @@ async function doSignup() {
242
228
  error.value = 'Please enter your email';
243
229
  return;
244
230
  }
245
- if (checkPassowrd()) {
231
+ if (!requestEmailConfirmation.value && checkPassword()) {
246
232
  validationRunning.value = true;
247
233
  return;
248
234
  }
@@ -254,7 +240,7 @@ async function doSignup() {
254
240
  body: {
255
241
  email,
256
242
  url: window.location.origin + window.location.pathname,
257
- password: password.value,
243
+ ...(!requestEmailConfirmation.value ? {password: password.value} : {}),
258
244
  }
259
245
  });
260
246
  inProgress.value = false;
@@ -264,15 +250,37 @@ async function doSignup() {
264
250
  error.value = null;
265
251
  requestSent.value = true;
266
252
  sentToEmail.value = email;
267
- if (resp.redirectTo) {
253
+ if (resp.redirectTo && !requestEmailConfirmation.value) {
268
254
  router.push(resp.redirectTo);
269
- } else {
255
+ } else if (!requestEmailConfirmation.value) {
270
256
  error.value = null;
271
257
  await user.finishLogin();
272
258
  }
273
259
  }
274
260
  }
275
261
 
276
-
277
-
262
+ const signupAfterEmailConfirmation = async () => {
263
+ if (checkPassword()) {
264
+ validationRunning.value = true;
265
+ return;
266
+ }
267
+ const resp = await callAdminForthApi({
268
+ path: `/plugin/${route.meta.pluginInstanceId}/complete-verified-signup`,
269
+ method: 'POST',
270
+ body: {
271
+ token: verifyToken.value,
272
+ password: password.value,
273
+ }
274
+ });
275
+ if (resp.error) {
276
+ window.adminforth.alert({
277
+ message: `Error fetching data: ${resp.error}`,
278
+ variant: 'danger',
279
+ });
280
+ } else if (resp.redirectTo) {
281
+ router.push(resp.redirectTo);
282
+ } else {
283
+ await user.finishLogin();
284
+ }
285
+ }
278
286
  </script>
package/dist/index.js CHANGED
@@ -8,7 +8,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
11
- import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses';
12
11
  export default class OpenSignupPlugin extends AdminForthPlugin {
13
12
  constructor(options) {
14
13
  super(options, import.meta.url);
@@ -23,9 +22,15 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
23
22
  if (!this.options.emailField) {
24
23
  throw new Error(`emailField is required and should be a name of field in auth resource`);
25
24
  }
25
+ // find field with name resourceConfig.emailField in adminforth.auth.usersResourceId and show error if it doesn't exist
26
+ const authResource = adminforth.config.resources.find(r => r.resourceId === adminforth.config.auth.usersResourceId);
27
+ if (!authResource) {
28
+ throw new Error(`Resource with id config.auth.usersResourceId=${adminforth.config.auth.usersResourceId} not found`);
29
+ }
30
+ this.authResource = authResource;
26
31
  if (this.options.confirmEmails) {
27
- if (!this.options.confirmEmails.emailProvider) {
28
- throw new Error(`confirmEmails.emailProvider is required and should be a name of field in auth resource`);
32
+ if (!this.options.confirmEmails.adapter) {
33
+ throw new Error(`confirmEmails.adapter is required and should be a name of field in auth resource`);
29
34
  }
30
35
  if (!this.options.confirmEmails.emailConfirmedField) {
31
36
  throw new Error(`confirmEmails.emailConfirmedField is required and should be a name of field in auth resource`);
@@ -38,16 +43,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
38
43
  `);
39
44
  }
40
45
  this.emailConfirmedField = emailConfirmedField;
41
- if (this.emailConfirmedField.type !== AdminForthDataTypes.BOOLEAN) {
42
- throw new Error(`Field ${this.emailConfirmedField.name} must be of type boolean`);
43
- }
44
46
  }
45
- // find field with name resourceConfig.emailField in adminforth.auth.usersResourceId and show error if it doesn't exist
46
- const authResource = adminforth.config.resources.find(r => r.resourceId === adminforth.config.auth.usersResourceId);
47
- if (!authResource) {
48
- throw new Error(`Resource with id config.auth.usersResourceId=${adminforth.config.auth.usersResourceId} not found`);
49
- }
50
- this.authResource = authResource;
51
47
  const emailField = authResource.columns.find(f => f.name === this.options.emailField);
52
48
  if (!emailField) {
53
49
  const similar = suggestIfTypo(authResource.columns.map(f => f.name), this.options.emailField);
@@ -81,7 +77,8 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
81
77
  minLength: passwordField.minLength,
82
78
  maxLength: passwordField.maxLength,
83
79
  validation: passwordField.validation
84
- }
80
+ },
81
+ requestEmailConfirmation: !!this.options.confirmEmails
85
82
  }
86
83
  }
87
84
  });
@@ -90,24 +87,23 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
90
87
  if (!adminforth.config.auth.beforeLoginConfirmation) {
91
88
  adminforth.config.auth.beforeLoginConfirmation = [];
92
89
  }
93
- // unshift because if e.g. 2fa set it's hook, this one should be first
94
- adminforth.config.auth.beforeLoginConfirmation.unshift((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
95
- if (!adminUser.dbUser[this.emailConfirmedField.name]) {
96
- return { body: { allowedLogin: false }, error: 'You need to confirm your email to be able to login' };
97
- }
98
- }));
99
90
  }
100
91
  });
101
92
  }
102
93
  validateConfigAfterDiscover(adminforth, resourceConfig) {
103
- // optional method where you can safely check field types after database discovery was performed
94
+ if (this.options.confirmEmails) {
95
+ const emailConfirmedColumn = this.resourceConfig.columns.find(f => f.name === this.options.confirmEmails.emailConfirmedField);
96
+ if (emailConfirmedColumn.type !== AdminForthDataTypes.BOOLEAN) {
97
+ throw new Error(`Field ${this.emailConfirmedField.name} must be of type boolean`);
98
+ }
99
+ }
104
100
  }
105
101
  instanceUniqueRepresentation(pluginOptions) {
106
102
  // optional method to return unique string representation of plugin instance.
107
103
  // Needed if plugin can have multiple instances on one resource
108
104
  return `single`;
109
105
  }
110
- doLogin(email, response) {
106
+ doLogin(email, response, extra) {
111
107
  return __awaiter(this, void 0, void 0, function* () {
112
108
  const username = email;
113
109
  const userRecord = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
@@ -117,7 +113,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
117
113
  username,
118
114
  };
119
115
  const toReturn = { allowedLogin: true, error: '' };
120
- yield this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response);
116
+ yield this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response, extra);
121
117
  if (toReturn.allowedLogin) {
122
118
  this.adminforth.auth.setAuthCookie({
123
119
  response,
@@ -131,29 +127,36 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
131
127
  setupEndpoints(server) {
132
128
  server.endpoint({
133
129
  method: 'POST',
134
- path: `/plugin/${this.pluginInstanceId}/completeVerifiedSignup`,
130
+ path: `/plugin/${this.pluginInstanceId}/complete-verified-signup`,
135
131
  noAuth: true,
136
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response }) {
137
- const { token } = body;
138
- const { email } = yield this.adminforth.auth.verify(token, 'tempVerifyEmailToken');
132
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, headers, query, cookies }) {
133
+ const { token, password } = body;
134
+ const { email } = yield this.adminforth.auth.verify(token, 'tempVerifyEmailToken', false);
139
135
  if (!email) {
140
136
  return { error: 'Invalid token', ok: false };
141
137
  }
138
+ if (!password) {
139
+ return { error: 'Password is required', ok: false };
140
+ }
142
141
  const userRecord = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
143
142
  if (!userRecord) {
144
143
  return { error: 'User not found', ok: false };
145
144
  }
145
+ if (userRecord[this.options.confirmEmails.emailConfirmedField]) {
146
+ return { error: 'Email already confirmed', ok: false };
147
+ }
146
148
  yield this.adminforth.resource(this.authResource.resourceId).update(userRecord[this.authResource.columns.find((col) => col.primaryKey).name], {
147
- [this.options.confirmEmails.emailConfirmedField]: true
149
+ [this.options.confirmEmails.emailConfirmedField]: true,
150
+ [this.options.passwordHashField]: yield AdminForth.Utils.generatePasswordHash(password),
148
151
  });
149
- return yield this.doLogin(email, response);
152
+ return yield this.doLogin(email, response, { body, headers, query, cookies });
150
153
  })
151
154
  });
152
155
  server.endpoint({
153
156
  method: 'POST',
154
157
  path: `/plugin/${this.pluginInstanceId}/signup`,
155
158
  noAuth: true,
156
- handler: (_b) => __awaiter(this, [_b], void 0, function* ({ body, response }) {
159
+ handler: (_b) => __awaiter(this, [_b], void 0, function* ({ body, response, headers, query, cookies }) {
157
160
  const { email, url, password } = body;
158
161
  // validate email
159
162
  if (this.emailField.validation) {
@@ -164,51 +167,39 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
164
167
  }
165
168
  }
166
169
  // validate password
167
- if (password.length < this.passwordField.minLength) {
168
- return { error: `Password must be at least ${this.passwordField.minLength} characters long`, ok: false };
169
- }
170
- if (password.length > this.passwordField.maxLength) {
171
- return { error: `Password must be at most ${this.passwordField.maxLength} characters long`, ok: false };
172
- }
173
- if (this.passwordField.validation) {
174
- for (const { regExp, message } of this.passwordField.validation) {
175
- if (!new RegExp(regExp).test(password)) {
176
- return { error: message, ok: false };
170
+ if (!this.options.confirmEmails) {
171
+ if (password.length < this.passwordField.minLength) {
172
+ return { error: `Password must be at least ${this.passwordField.minLength} characters long`, ok: false };
173
+ }
174
+ if (password.length > this.passwordField.maxLength) {
175
+ return { error: `Password must be at most ${this.passwordField.maxLength} characters long`, ok: false };
176
+ }
177
+ if (this.passwordField.validation) {
178
+ for (const { regExp, message } of this.passwordField.validation) {
179
+ if (!new RegExp(regExp).test(password)) {
180
+ return { error: message, ok: false };
181
+ }
177
182
  }
178
183
  }
179
184
  }
180
185
  // first check again if email already exists
181
- const existing = yield this.adminforth.resource(this.authResource.resourceId).count(Filters.EQ(this.emailField.name, email));
182
- if (existing > 0) {
186
+ const existingUser = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
187
+ if ((!this.options.confirmEmails && existingUser) || (this.options.confirmEmails && (existingUser === null || existingUser === void 0 ? void 0 : existingUser[this.emailConfirmedField.name]))) {
183
188
  return { error: 'Email already exists', ok: false };
184
189
  }
185
190
  // create user
186
- const created = yield this.adminforth.resource(this.authResource.resourceId).create(Object.assign(Object.assign(Object.assign({}, (this.options.defaultFieldValues || {})), (this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {})), { [this.emailField.name]: email, [this.options.passwordHashField]: yield AdminForth.Utils.generatePasswordHash(password) }));
191
+ if (!existingUser) {
192
+ const created = yield this.adminforth.resource(this.authResource.resourceId).create(Object.assign(Object.assign(Object.assign({}, (this.options.defaultFieldValues || {})), (this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {})), { [this.emailField.name]: email, [this.options.passwordHashField]: password ? yield AdminForth.Utils.generatePasswordHash(password) : '' }));
193
+ }
187
194
  if (!this.options.confirmEmails) {
188
- const resp = yield this.doLogin(email, response);
195
+ const resp = yield this.doLogin(email, response, { body, headers, query, cookies });
189
196
  return resp;
190
197
  }
191
198
  // send confirmation email
192
199
  const brandName = this.adminforth.config.customization.brandName;
193
200
  const verifyToken = this.adminforth.auth.issueJWT({ email, issuer: brandName }, 'tempVerifyEmailToken', '2h');
194
- // send email with AWS SES this.options.providerOptions.AWS_SES
195
- const ses = new SESClient({
196
- region: this.options.confirmEmails.providerOptions.AWS_SES.region,
197
- credentials: {
198
- accessKeyId: this.options.confirmEmails.providerOptions.AWS_SES.accessKeyId,
199
- secretAccessKey: this.options.confirmEmails.providerOptions.AWS_SES.secretAccessKey
200
- }
201
- });
202
201
  process.env.HEAVY_DEBUG && console.log('🐛Sending reset tok to', verifyToken);
203
- const emailCommand = new SendEmailCommand({
204
- Destination: {
205
- ToAddresses: [email]
206
- },
207
- Message: {
208
- Body: {
209
- Text: {
210
- Charset: "UTF-8",
211
- Data: `
202
+ const emailText = `
212
203
  Dear user,
213
204
  Welcome to ${brandName}!
214
205
 
@@ -222,11 +213,8 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
222
213
  Thanks,
223
214
  The ${brandName} Team
224
215
 
225
- `
226
- },
227
- Html: {
228
- Charset: "UTF-8",
229
- Data: `
216
+ `;
217
+ const emailHtml = `
230
218
  <html>
231
219
  <head></head>
232
220
  <body>
@@ -242,26 +230,10 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
242
230
  </html>
243
231
 
244
232
 
245
- `
246
- }
247
- },
248
- Subject: {
249
- Charset: "UTF-8",
250
- Data: `Signup request at ${brandName}`
251
- }
252
- },
253
- Source: `${this.options.confirmEmails.sendFrom}`
254
- });
255
- try {
256
- const sRes = yield ses.send(emailCommand);
257
- }
258
- catch (e) {
259
- console.error('Error sending email', e);
260
- if (process.env.NODE_ENV === 'development') {
261
- return { error: 'Some thing went wrong, please check the console', ok: false };
262
- }
263
- return { error: 'Something went wrong, please contact support', ok: false };
264
- }
233
+ `;
234
+ const emailSubject = `Signup request at ${brandName}`;
235
+ // send email with AWS SES this.options.providerOptions.AWS_SES
236
+ this.options.confirmEmails.adapter.sendEmail(this.options.confirmEmails.sendFrom, email, emailText, emailHtml, emailSubject);
265
237
  return { ok: true };
266
238
  })
267
239
  });
package/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
2
- import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction } from "adminforth";
2
+ import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, HttpExtra } from "adminforth";
3
3
  import type { PluginOptions } from './types.js';
4
- import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses';
5
4
 
6
5
 
7
6
  export default class OpenSignupPlugin extends AdminForthPlugin {
@@ -25,9 +24,16 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
25
24
  throw new Error(`emailField is required and should be a name of field in auth resource`);
26
25
  }
27
26
 
27
+ // find field with name resourceConfig.emailField in adminforth.auth.usersResourceId and show error if it doesn't exist
28
+ const authResource = adminforth.config.resources.find(r => r.resourceId === adminforth.config.auth.usersResourceId);
29
+ if (!authResource) {
30
+ throw new Error(`Resource with id config.auth.usersResourceId=${adminforth.config.auth.usersResourceId} not found`);
31
+ }
32
+ this.authResource = authResource;
33
+
28
34
  if (this.options.confirmEmails) {
29
- if (!this.options.confirmEmails.emailProvider) {
30
- throw new Error(`confirmEmails.emailProvider is required and should be a name of field in auth resource`);
35
+ if (!this.options.confirmEmails.adapter) {
36
+ throw new Error(`confirmEmails.adapter is required and should be a name of field in auth resource`);
31
37
  }
32
38
  if (!this.options.confirmEmails.emailConfirmedField) {
33
39
  throw new Error(`confirmEmails.emailConfirmedField is required and should be a name of field in auth resource`);
@@ -40,18 +46,8 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
40
46
  `);
41
47
  }
42
48
  this.emailConfirmedField = emailConfirmedField;
43
- if (this.emailConfirmedField.type !== AdminForthDataTypes.BOOLEAN) {
44
- throw new Error(`Field ${this.emailConfirmedField.name} must be of type boolean`);
45
- }
46
49
  }
47
50
 
48
- // find field with name resourceConfig.emailField in adminforth.auth.usersResourceId and show error if it doesn't exist
49
- const authResource = adminforth.config.resources.find(r => r.resourceId === adminforth.config.auth.usersResourceId);
50
- if (!authResource) {
51
- throw new Error(`Resource with id config.auth.usersResourceId=${adminforth.config.auth.usersResourceId} not found`);
52
- }
53
- this.authResource = authResource;
54
-
55
51
  const emailField = authResource.columns.find(f => f.name === this.options.emailField);
56
52
  if (!emailField) {
57
53
  const similar = suggestIfTypo(authResource.columns.map(f => f.name), this.options.emailField);
@@ -89,7 +85,8 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
89
85
  minLength: passwordField.minLength,
90
86
  maxLength: passwordField.maxLength,
91
87
  validation: passwordField.validation
92
- }
88
+ },
89
+ requestEmailConfirmation: !!this.options.confirmEmails
93
90
  }
94
91
  }
95
92
  });
@@ -99,19 +96,16 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
99
96
  if (!adminforth.config.auth.beforeLoginConfirmation) {
100
97
  adminforth.config.auth.beforeLoginConfirmation = [];
101
98
  }
102
- // unshift because if e.g. 2fa set it's hook, this one should be first
103
- (adminforth.config.auth.beforeLoginConfirmation as BeforeLoginConfirmationFunction[]).unshift(
104
- async ({ adminUser }) => {
105
- if (!adminUser.dbUser[this.emailConfirmedField.name]) {
106
- return { body: { allowedLogin: false }, error: 'You need to confirm your email to be able to login' };
107
- }
108
- }
109
- );
110
99
  }
111
100
  }
112
101
 
113
102
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
114
- // optional method where you can safely check field types after database discovery was performed
103
+ if(this.options.confirmEmails){
104
+ const emailConfirmedColumn = this.resourceConfig.columns.find(f => f.name === this.options.confirmEmails.emailConfirmedField);
105
+ if (emailConfirmedColumn.type !== AdminForthDataTypes.BOOLEAN) {
106
+ throw new Error(`Field ${this.emailConfirmedField.name} must be of type boolean`);
107
+ }
108
+ }
115
109
  }
116
110
 
117
111
  instanceUniqueRepresentation(pluginOptions: any) : string {
@@ -120,7 +114,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
120
114
  return `single`;
121
115
  }
122
116
 
123
- async doLogin(email: string, response: any): Promise<{ error?: string; allowedLogin: boolean; redirectTo?: string; }> {
117
+ async doLogin(email: string, response: any, extra: HttpExtra): Promise<{ error?: string; allowedLogin: boolean; redirectTo?: string; }> {
124
118
 
125
119
  const username = email;
126
120
  const userRecord = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
@@ -131,7 +125,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
131
125
  };
132
126
  const toReturn = { allowedLogin: true, error: '' };
133
127
 
134
- await this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response);
128
+ await this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response, extra);
135
129
  if (toReturn.allowedLogin) {
136
130
  this.adminforth.auth.setAuthCookie({
137
131
  response,
@@ -149,22 +143,32 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
149
143
  setupEndpoints(server: IHttpServer) {
150
144
  server.endpoint({
151
145
  method: 'POST',
152
- path: `/plugin/${this.pluginInstanceId}/completeVerifiedSignup`,
146
+ path: `/plugin/${this.pluginInstanceId}/complete-verified-signup`,
153
147
  noAuth: true,
154
- handler: async ({ body, response }) => {
155
- const { token } = body;
156
- const { email } = await this.adminforth.auth.verify(token, 'tempVerifyEmailToken');
148
+ handler: async ({ body, response, headers, query, cookies }) => {
149
+ const { token, password } = body;
150
+ const { email } = await this.adminforth.auth.verify(token, 'tempVerifyEmailToken', false);
157
151
  if (!email) {
158
152
  return { error: 'Invalid token', ok: false };
159
153
  }
154
+
155
+ if(!password) {
156
+ return { error: 'Password is required', ok: false };
157
+ }
160
158
  const userRecord = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
161
159
  if (!userRecord) {
162
160
  return { error: 'User not found', ok: false };
163
161
  }
162
+
163
+ if (userRecord[this.options.confirmEmails.emailConfirmedField]) {
164
+ return { error: 'Email already confirmed', ok: false };
165
+ }
166
+
164
167
  await this.adminforth.resource(this.authResource.resourceId).update(userRecord[this.authResource.columns.find((col) => col.primaryKey).name], {
165
- [this.options.confirmEmails.emailConfirmedField]: true
168
+ [this.options.confirmEmails.emailConfirmedField]: true,
169
+ [this.options.passwordHashField]: await AdminForth.Utils.generatePasswordHash(password),
166
170
  });
167
- return await this.doLogin(email, response);
171
+ return await this.doLogin(email, response, { body, headers, query, cookies });
168
172
  }
169
173
  });
170
174
 
@@ -172,7 +176,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
172
176
  method: 'POST',
173
177
  path: `/plugin/${this.pluginInstanceId}/signup`,
174
178
  noAuth: true,
175
- handler: async ({ body, response }) => {
179
+ handler: async ({ body, response, headers, query, cookies }) => {
176
180
  const { email, url, password } = body;
177
181
 
178
182
  // validate email
@@ -185,36 +189,40 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
185
189
  }
186
190
 
187
191
  // validate password
188
- if (password.length < this.passwordField.minLength) {
189
- return { error: `Password must be at least ${this.passwordField.minLength} characters long`, ok: false };
190
- }
191
- if (password.length > this.passwordField.maxLength) {
192
- return { error: `Password must be at most ${this.passwordField.maxLength} characters long`, ok: false };
193
- }
194
- if (this.passwordField.validation) {
195
- for (const { regExp, message } of this.passwordField.validation) {
196
- if (!new RegExp(regExp).test(password)) {
197
- return { error: message, ok: false };
192
+ if (!this.options.confirmEmails) {
193
+ if (password.length < this.passwordField.minLength) {
194
+ return { error: `Password must be at least ${this.passwordField.minLength} characters long`, ok: false };
195
+ }
196
+ if (password.length > this.passwordField.maxLength) {
197
+ return { error: `Password must be at most ${this.passwordField.maxLength} characters long`, ok: false };
198
+ }
199
+ if (this.passwordField.validation) {
200
+ for (const { regExp, message } of this.passwordField.validation) {
201
+ if (!new RegExp(regExp).test(password)) {
202
+ return { error: message, ok: false };
203
+ }
198
204
  }
199
205
  }
200
206
  }
201
207
 
202
208
  // first check again if email already exists
203
- const existing = await this.adminforth.resource(this.authResource.resourceId).count(Filters.EQ(this.emailField.name, email));
204
- if (existing > 0) {
209
+ const existingUser = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
210
+ if ((!this.options.confirmEmails && existingUser) || (this.options.confirmEmails && existingUser?.[this.emailConfirmedField.name])) {
205
211
  return { error: 'Email already exists', ok: false };
206
212
  }
207
213
 
208
214
  // create user
209
- const created = await this.adminforth.resource(this.authResource.resourceId).create({
210
- ...(this.options.defaultFieldValues || {}),
211
- ...(this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {}),
212
- [this.emailField.name]: email,
213
- [this.options.passwordHashField]: await AdminForth.Utils.generatePasswordHash(password),
214
- });
215
+ if (!existingUser) {
216
+ const created = await this.adminforth.resource(this.authResource.resourceId).create({
217
+ ...(this.options.defaultFieldValues || {}),
218
+ ...(this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {}),
219
+ [this.emailField.name]: email,
220
+ [this.options.passwordHashField]: password ? await AdminForth.Utils.generatePasswordHash(password) : '',
221
+ });
222
+ }
215
223
 
216
224
  if (!this.options.confirmEmails) {
217
- const resp = await this.doLogin(email, response);
225
+ const resp = await this.doLogin(email, response, { body, headers, query, cookies });
218
226
  return resp;
219
227
  }
220
228
 
@@ -223,27 +231,8 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
223
231
  const brandName = this.adminforth.config.customization.brandName;
224
232
 
225
233
  const verifyToken = this.adminforth.auth.issueJWT({email, issuer: brandName }, 'tempVerifyEmailToken', '2h');
226
-
227
- // send email with AWS SES this.options.providerOptions.AWS_SES
228
- const ses = new SESClient({
229
- region: this.options.confirmEmails.providerOptions.AWS_SES.region,
230
- credentials: {
231
- accessKeyId: this.options.confirmEmails.providerOptions.AWS_SES.accessKeyId,
232
- secretAccessKey: this.options.confirmEmails.providerOptions.AWS_SES.secretAccessKey
233
- }
234
- });
235
-
236
234
  process.env.HEAVY_DEBUG && console.log('🐛Sending reset tok to', verifyToken);
237
-
238
- const emailCommand = new SendEmailCommand({
239
- Destination: {
240
- ToAddresses: [email]
241
- },
242
- Message: {
243
- Body: {
244
- Text: {
245
- Charset: "UTF-8",
246
- Data: `
235
+ const emailText = `
247
236
  Dear user,
248
237
  Welcome to ${brandName}!
249
238
 
@@ -257,11 +246,9 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
257
246
  Thanks,
258
247
  The ${brandName} Team
259
248
 
260
- `
261
- },
262
- Html: {
263
- Charset: "UTF-8",
264
- Data: `
249
+ `;
250
+
251
+ const emailHtml = `
265
252
  <html>
266
253
  <head></head>
267
254
  <body>
@@ -277,26 +264,11 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
277
264
  </html>
278
265
 
279
266
 
280
- `
281
- }
282
- },
283
- Subject: {
284
- Charset: "UTF-8",
285
- Data: `Signup request at ${brandName}`
286
- }
287
- },
288
- Source: `${this.options.confirmEmails.sendFrom}`
289
- });
267
+ `;
268
+ const emailSubject = `Signup request at ${brandName}`;
290
269
 
291
- try {
292
- const sRes = await ses.send(emailCommand);
293
- } catch (e) {
294
- console.error('Error sending email', e);
295
- if (process.env.NODE_ENV === 'development') {
296
- return { error: 'Some thing went wrong, please check the console', ok: false };
297
- }
298
- return { error: 'Something went wrong, please contact support', ok: false };
299
- }
270
+ // send email with AWS SES this.options.providerOptions.AWS_SES
271
+ this.options.confirmEmails.adapter.sendEmail(this.options.confirmEmails.sendFrom, email, emailText, emailHtml, emailSubject);
300
272
 
301
273
  return { ok: true };
302
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/open-signup",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { EmailAdapter } from 'adminforth';
2
+
1
3
  export interface PluginOptions {
2
4
 
3
5
  /**
@@ -40,11 +42,6 @@ export interface PluginOptions {
40
42
  */
41
43
  emailConfirmedField: string;
42
44
 
43
- /**
44
- * Required to confirm email
45
- */
46
- emailProvider: 'AWS_SES' ;
47
-
48
45
  /**
49
46
  * From which email to send password reset emails
50
47
  * e.g. no-reply@example.com
@@ -53,15 +50,9 @@ export interface PluginOptions {
53
50
  sendFrom: string;
54
51
 
55
52
  /**
56
- * Options for email provider
53
+ * Adapter to send emails
57
54
  */
58
- providerOptions: {
59
- AWS_SES: {
60
- region: string;
61
- accessKeyId: string;
62
- secretAccessKey: string;
63
- }
64
- }
55
+ adapter: EmailAdapter;
65
56
  }
66
57
 
67
58
 
@@ -1,295 +0,0 @@
1
- <template>
2
- <div class="relative flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-800 relative w-screen h-screen"
3
- :style="coreStore.config?.loginBackgroundImage && backgroundPosition === 'over' ? {
4
- 'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
- 'background-size': 'cover',
6
- 'background-position': 'center',
7
- 'background-blend-mode': 'darken'
8
- }: {}"
9
- >
10
-
11
- <img v-if="coreStore.config?.loginBackgroundImage && backgroundPosition !== 'over'"
12
- :src="loadFile(coreStore.config?.loginBackgroundImage)"
13
- class="position-absolute top-0 left-0 h-screen object-cover w-0"
14
- :class="{
15
- '1/2': 'md:w-1/2',
16
- '1/3': 'md:w-1/3',
17
- '2/3': 'md:w-2/3',
18
- '3/4': 'md:w-3/4',
19
- '2/5': 'md:w-2/5',
20
- '3/5': 'md:w-3/5',
21
- }[backgroundPosition]"
22
- />
23
-
24
- <!-- Main modal -->
25
- <div id="authentication-modal" tabindex="-1"
26
- class="overflow-y-auto flex flex-grow
27
- overflow-x-hidden z-50 min-w-[350px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
28
- <div class="relative p-4 w-full max-h-full max-w-[400px]">
29
- <!-- Modal content -->
30
- <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
31
- <!-- Modal header -->
32
- <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
33
- <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
34
- Reset your password on {{ coreStore.config?.brandName }}
35
- </h3>
36
- </div>
37
- <!-- Modal body -->
38
- <div class="p-4 md:p-5">
39
- <form v-if="enteringNew" class="space-y-4" role="alert" @submit.prevent>
40
- <div class="relative">
41
- <label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New password</label>
42
- <input
43
- :type="unmasked ? 'text' : 'password'"
44
- name="password" id="password"
45
- v-model="password"
46
- @keydown.enter="passwordConfirmationInput.focus()"
47
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
48
- placeholder="New password" required
49
- />
50
-
51
- <button
52
- type="button"
53
- @click="unmasked = !unmasked"
54
- class="h-6 absolute inset-y-2 top-9 right-1 flex items-center pr-2 z-index-100 focus:outline-none"
55
- >
56
- <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked" />
57
- <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
58
- </button>
59
- </div>
60
-
61
- <div class="relative">
62
- <label for="password_confirmation" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm new password</label>
63
- <input
64
- :type="unmasked ? 'text' : 'password'"
65
- name="password_confirmation" id="password_confirmation"
66
- v-model="passwordConfirmation"
67
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
68
- placeholder="Confirm new password" required
69
- />
70
-
71
- <button
72
- type="button"
73
- @click="unmasked = !unmasked"
74
- class="h-6 absolute inset-y-2 top-9 right-1 flex items-center pr-2 z-index-100 focus:outline-none"
75
- >
76
- <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked" />
77
- <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
78
- </button>
79
- </div>
80
-
81
- <div v-if="validationError || error" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
82
- <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
83
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
84
- </svg>
85
- <span class="sr-only">Info</span>
86
- <div>
87
- {{ validationError || error }}
88
- </div>
89
- </div>
90
-
91
- <button
92
- :disabled="inProgress || (validationRunning && validationError)"
93
- @click="setNewPassword"
94
- type="submit" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 disabled:opacity-50">
95
- <svg v-if="inProgress"
96
- aria-hidden="true" class="w-4 h-4 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874
97
- // 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
98
- Set new password
99
- </button>
100
- </form>
101
- <!-- END of set new paasord -->
102
- <div v-if="!enteringNew && requestSent" class="flex items center justify-center p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-green-800 dark:text-green-400" role="alert">
103
- If user with specified email exists in our db, then request was sent. Please check your email at {{ sentToEmail }} to reset your password.
104
- </div>
105
-
106
- <form v-if="!enteringNew && !requestSent" class="space-y-4" @submit.prevent>
107
- <div>
108
- <label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
109
- <input type="email" name="email" id="email"
110
- @keydown.enter="reset()"
111
- ref="emailInput"
112
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
113
- placeholder="name@company.com"
114
- required
115
- />
116
- </div>
117
-
118
- <div v-if="error" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
119
- <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
120
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
121
- </svg>
122
- <span class="sr-only">Info</span>
123
- <div>
124
- {{ error }}
125
- </div>
126
- </div>
127
-
128
-
129
- <button
130
- @click="reset"
131
- type="submit" class="flex items-center justify-center gap-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
132
- <svg v-if="inProgress"
133
- aria-hidden="true" class="w-4 h-4 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
134
-
135
- Reset Password
136
-
137
- </button>
138
-
139
- <p class="text-gray-500 dark:text-gray-400 font-sm text-right mt-3">
140
- or <router-link class=" text-blue-600 underline dark:text-blue-500 hover:no-underline
141
- cursor-pointer" to="/login">
142
- Back to login
143
- </router-link>
144
- </p>
145
-
146
- </form>
147
-
148
- </div>
149
- </div>
150
- </div>
151
- </div>
152
-
153
- </div>
154
- </template>
155
-
156
-
157
- <script setup lang="ts">
158
-
159
- import { onMounted, ref, computed, Ref } from 'vue';
160
- import { useCoreStore } from '@/stores/core';
161
- import { callAdminForthApi, loadFile, applyRegexValidation } from '@/utils';
162
- import { useRoute, useRouter } from 'vue-router';
163
- import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
164
-
165
-
166
- const inProgress = ref(false);
167
-
168
- const coreStore = useCoreStore();
169
- const requestSent = ref(false);
170
-
171
- const emailInput = ref(null);
172
- const password = ref("");
173
- const passwordConfirmation = ref("");
174
- const unmasked = ref(false);
175
- const sentToEmail: Ref<string> = ref('');
176
-
177
-
178
- const route = useRoute();
179
- const router = useRouter();
180
-
181
- function checkPassowrd() {
182
-
183
- if (!password.value || !passwordConfirmation.value) {
184
- return 'Please enter both password and password confirmation';
185
- }
186
- if (password.value !== passwordConfirmation.value) {
187
- return 'Passwords do not match';
188
- }
189
-
190
- if (password.value.length < passwordField.value.minLength) {
191
- return `Password must be at least ${passwordField.value.minLength} characters long`;
192
- }
193
-
194
- if (password.value.length > passwordField.value.maxLength) {
195
- return `Password must be at most ${passwordField.value.maxLength} characters long`;
196
- }
197
-
198
- if (passwordField.value.validation) {
199
- const valError = applyRegexValidation(password.value, passwordField.value.validation);
200
- if (valError) {
201
- return valError;
202
- }
203
- }
204
-
205
- return null;
206
- }
207
-
208
- const validationRunning = ref(false);
209
-
210
- const validationError = computed(() => {
211
- console.log('validationRunning.value', validationRunning.value, 'aa', checkPassowrd());
212
- if (validationRunning.value) {
213
- return checkPassowrd();
214
- }
215
- return null;
216
- });
217
-
218
- const error = ref(null);
219
-
220
- // if token query param is present, we are entering new password
221
- const enteringNew = computed(() => route.query.token);
222
-
223
- const backgroundPosition = computed(() => {
224
- return coreStore.config?.loginBackgroundPosition || '1/2';
225
- });
226
-
227
- const passwordField = computed(
228
- () => route.meta.passwordField
229
- )
230
-
231
- onMounted(async () => {
232
- await coreStore.getPublicConfig();
233
- });
234
-
235
- async function reset() {
236
- error.value = null;
237
- const email = emailInput.value.value;
238
- if (!email) {
239
- error.value = 'Please enter your email';
240
- return;
241
- }
242
-
243
- inProgress.value = true;
244
- const resp = await callAdminForthApi({
245
- path: `/plugin/${route.meta.pluginInstanceId}/reset-password`,
246
- method: 'POST',
247
- body: {
248
- email,
249
- url: window.location.origin + window.location.pathname,
250
- }
251
- });
252
- inProgress.value = false;
253
- if (resp.error) {
254
- error.value = resp.error;
255
- } else {
256
- error.value = null;
257
- requestSent.value = true;
258
- sentToEmail.value = email;
259
- }
260
- }
261
-
262
- async function setNewPassword() {
263
- error.value = null;
264
-
265
- if (checkPassowrd()) {
266
- validationRunning.value = true;
267
- return;
268
- }
269
-
270
- inProgress.value = true;
271
- const resp = await callAdminForthApi({
272
- path: `/plugin/${route.meta.pluginInstanceId}/reset-password-confirm`,
273
- method: 'POST',
274
- body: {
275
- token: route.query.token,
276
- password: password.value,
277
- }
278
- });
279
- inProgress.value = false;
280
- if (resp.error) {
281
- error.value = resp.error;
282
- } else {
283
- error.value = null;
284
- router.push('/login');
285
- window.adminforth.alert({
286
- message: 'Password reset successfully. Please login with your new password',
287
- variant: 'success',
288
- timeout: 15,
289
- });
290
-
291
- }
292
- }
293
-
294
-
295
- </script>
@@ -1,13 +0,0 @@
1
- <template>
2
- <p class="text-gray-500 dark:text-gray-400 font-sm text-left mt-3">
3
- Forgot password? <router-link class=" text-blue-600 underline dark:text-blue-500 hover:no-underline
4
- cursor-pointer" to="/reset-password">
5
- reset it
6
- </router-link>
7
- </p>
8
- </template>
9
-
10
- <script setup>
11
-
12
-
13
- </script>