@adminforth/open-signup 1.0.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.
@@ -0,0 +1,337 @@
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
+ {{$t('Sign up into')}} {{ coreStore.config?.brandName }}
35
+ </h3>
36
+ </div>
37
+ <!-- Modal body -->
38
+ <div class="p-4 md:p-5">
39
+ <form v-if="!requestSent" class="space-y-4" role="alert" @submit.prevent>
40
+ <div v-if="!verifyToken" class="relative">
41
+ <label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{$t('Your email address')}}</label>
42
+ <input type="email" name="email" id="email"
43
+ tabindex="1"
44
+ autocomplete="username"
45
+ @keydown.enter="passwordInput?.focus()"
46
+ ref="emailInput"
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="name@company.com"
49
+ required
50
+ />
51
+ </div>
52
+ <div v-if="isPasswordNeeded" class="relative">
53
+ <label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{$t('Your password')}}</label>
54
+ <input
55
+ tabindex="2"
56
+ autocomplete="new-password"
57
+ ref="passwordInput"
58
+ :type="unmasked ? 'text' : 'password'"
59
+ name="password" id="password"
60
+ v-model="password"
61
+ @keydown.enter="passwordConfirmationInput?.focus()"
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"
63
+ :placeholder="$t('New password')" required
64
+ />
65
+
66
+ <button
67
+ type="button"
68
+ @click="unmasked = !unmasked"
69
+ class="h-6 absolute inset-y-2 top-9 right-1 flex items-center pr-2 z-index-100 focus:outline-none"
70
+ >
71
+ <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked" />
72
+ <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
73
+ </button>
74
+ </div>
75
+
76
+ <div v-if="isPasswordNeeded" class="relative">
77
+ <label for="password_confirmation" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{$t('Confirm your password')}}</label>
78
+ <input
79
+ ref="passwordConfirmationInput"
80
+ autocomplete="new-password"
81
+ :type="unmasked ? 'text' : 'password'"
82
+ name="password_confirmation" id="password_confirmation"
83
+ tabindex="3"
84
+ v-model="passwordConfirmation"
85
+ 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"
86
+ :placeholder="$t('Confirm new password')" required
87
+ />
88
+
89
+ <button
90
+ type="button"
91
+ @click="unmasked = !unmasked"
92
+ class="h-6 absolute inset-y-2 top-9 right-1 flex items-center pr-2 z-index-100 focus:outline-none"
93
+ >
94
+ <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked" />
95
+ <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
96
+ </button>
97
+ </div>
98
+
99
+ <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">
100
+ <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">
101
+ <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"/>
102
+ </svg>
103
+ <span class="sr-only">{{$t('Info')}}</span>
104
+ <div>
105
+ {{ validationError || error }}
106
+ </div>
107
+ </div>
108
+
109
+ <Button
110
+ :disabled="inProgress || (validationRunning && validationError)"
111
+ @click="() => verifyToken ? signupAfterEmailConfirmation() : doSignup()"
112
+ type="submit"
113
+ class="w-full"
114
+ :loader="inProgress"
115
+ >
116
+ {{$t('Sign up')}}
117
+ </Button>
118
+ </form>
119
+ <!-- END of set new paasord -->
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">
121
+ {{$t('Please check your email at')}} {{ sentToEmail }} {{$t('to confirm your email address.')}}
122
+ </div>
123
+ <p class="text-gray-500 dark:text-gray-400 font-sm text-right mt-3">
124
+ {{$t('Already have an account?')}} <Link :to="`/login${route.query.next ? `?next=${encodeURIComponent(route.query.next)}` : ''}`">{{ $t('login here') }}</Link>
125
+ </p>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ </div>
132
+ </template>
133
+
134
+
135
+ <script setup lang="ts">
136
+ import { onMounted, ref, computed, Ref, onBeforeMount, nextTick } from 'vue';
137
+ import { useCoreStore } from '@/stores/core';
138
+ import { useUserStore } from '@/stores/user';
139
+ import { callAdminForthApi, loadFile, applyRegexValidation } from '@/utils';
140
+ import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
141
+ import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
142
+ import Button from '@/afcl/Button.vue';
143
+ import Link from '@/afcl/Link.vue';
144
+ import { useI18n } from 'vue-i18n';
145
+ import adminforth from '@/adminforth';
146
+
147
+ const { t } = useI18n();
148
+
149
+ const inProgress = ref(false);
150
+
151
+ const coreStore = useCoreStore();
152
+ const requestSent = ref(false);
153
+
154
+ const emailInput: Ref<HTMLInputElement | null> = ref(null);
155
+ const passwordInput: Ref<HTMLInputElement | null> = ref(null);
156
+ const passwordConfirmationInput: Ref<HTMLInputElement | null> = ref(null);
157
+
158
+ const password = ref("");
159
+ const passwordConfirmation = ref("");
160
+ const unmasked = ref(false);
161
+ const sentToEmail: Ref<string> = ref('');
162
+
163
+ const requestEmailConfirmation = computed(() => route.meta.requestEmailConfirmation);
164
+ const verifyToken = computed(() => route.query.token);
165
+ const isPasswordNeeded = computed(() => !requestEmailConfirmation.value || (requestEmailConfirmation.value && verifyToken.value));
166
+
167
+ const user = useUserStore();
168
+
169
+ const route = useRoute();
170
+ const router = useRouter();
171
+
172
+ function checkPassword() {
173
+
174
+ if (!password.value || !passwordConfirmation.value) {
175
+ return t('Please enter both password and password confirmation');
176
+ }
177
+ if (password.value !== passwordConfirmation.value) {
178
+ return t('Passwords do not match');
179
+ }
180
+
181
+ if (!passwordConstraints.value) {
182
+ return null;
183
+ }
184
+ if (password.value.length < passwordConstraints.value.minLength) {
185
+ return t(`Password must be at least {minLength} characters long`, { minLength: passwordConstraints.value.minLength });
186
+ }
187
+
188
+ if (password.value.length > passwordConstraints.value.maxLength) {
189
+ return t(`Password must be at most {maxLength} characters long`, { maxLength: passwordConstraints.value.maxLength });
190
+ }
191
+
192
+ if (passwordConstraints.value.validation) {
193
+ const valError = applyRegexValidation(password.value, passwordConstraints.value.validation);
194
+ if (valError) {
195
+ return valError;
196
+ }
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ const validationRunning = ref(false);
203
+
204
+ const validationError = computed(() => {
205
+ if (validationRunning.value) {
206
+ return checkPassword();
207
+ }
208
+ return null;
209
+ });
210
+
211
+ const error: Ref<string | null> = ref(null);
212
+
213
+
214
+ const backgroundPosition = computed(() => {
215
+ return coreStore.config?.loginBackgroundPosition || '1/2';
216
+ });
217
+
218
+
219
+ const passwordConstraints: Ref<{
220
+ minLength: number;
221
+ maxLength: number;
222
+ validation: string;
223
+ }> = ref({
224
+ minLength: 8,
225
+ maxLength: 100,
226
+ validation: '',
227
+ });
228
+
229
+ // implement something similar to beforeEnter
230
+ // beforeEnter: async (to, from, next) => {
231
+ // if(localStorage.getItem('isAuthorized') === 'true') {
232
+ // // check if url has next=... and redirect to it
233
+ // console.log('to.query', to.query)
234
+ // if (to.query.next) {
235
+ // next(to.query.next.toString())
236
+ // } else {
237
+ // next({name: 'home'});
238
+ // }
239
+ // } else {
240
+ // next()
241
+ // }
242
+ // }
243
+
244
+ onBeforeMount(() => {
245
+ // console.log('⛔ SignUp Page. Before Mount: ', route)
246
+ if (localStorage.getItem('isAuthorized') === 'true') {
247
+ // if route has next param, redirect
248
+ coreStore.fetchMenuAndResource();
249
+ if (route.query.next) {
250
+ router.push(route.query.next.toString());
251
+ } else {
252
+ router.push({ name: 'home' });
253
+ }
254
+ }
255
+ })
256
+
257
+ onMounted(async () => {
258
+ emailInput.value?.focus();
259
+ await coreStore.getPublicConfig();
260
+
261
+ await nextTick();
262
+ await router.isReady();
263
+
264
+ // getPasswordConstraints
265
+ console.log('⛔ SignUp Page. Before Mount: ', route.meta)
266
+ passwordConstraints.value = await callAdminForthApi({
267
+ path: `/plugin/${route.meta.pluginInstanceId}/password-constraints`,
268
+ method: 'GET',
269
+ });
270
+ })
271
+
272
+ // onMounted(async () => {
273
+ // });
274
+
275
+ async function doSignup() {
276
+ error.value = null;
277
+ const email = emailInput.value!.value;
278
+ if (!email) {
279
+ error.value = t('Please enter your email');
280
+ return;
281
+ }
282
+ if (!requestEmailConfirmation.value && checkPassword()) {
283
+ validationRunning.value = true;
284
+ return;
285
+ }
286
+
287
+ inProgress.value = true;
288
+ const resp = await callAdminForthApi({
289
+ path: `/plugin/${route.meta.pluginInstanceId}/signup`,
290
+ method: 'POST',
291
+ body: {
292
+ email,
293
+ url: window.location.origin + window.location.pathname,
294
+ ...(!requestEmailConfirmation.value ? {password: password.value} : {}),
295
+ }
296
+ });
297
+ inProgress.value = false;
298
+ if (resp.error) {
299
+ error.value = resp.error;
300
+ } else {
301
+ error.value = null;
302
+ requestSent.value = true;
303
+ sentToEmail.value = email;
304
+ if (resp.redirectTo && !requestEmailConfirmation.value) {
305
+ router.push(resp.redirectTo);
306
+ } else if (!requestEmailConfirmation.value) {
307
+ error.value = null;
308
+ await user.finishLogin();
309
+ }
310
+ }
311
+ }
312
+
313
+ const signupAfterEmailConfirmation = async () => {
314
+ if (checkPassword()) {
315
+ validationRunning.value = true;
316
+ return;
317
+ }
318
+ const resp = await callAdminForthApi({
319
+ path: `/plugin/${route.meta.pluginInstanceId}/complete-verified-signup`,
320
+ method: 'POST',
321
+ body: {
322
+ token: verifyToken.value,
323
+ password: password.value,
324
+ }
325
+ });
326
+ if (resp.error) {
327
+ adminforth.alert({
328
+ message: t(`Error fetching data: {error}`, { error: resp.error }),
329
+ variant: 'danger',
330
+ });
331
+ } else if (resp.redirectTo) {
332
+ router.push(resp.redirectTo);
333
+ } else {
334
+ await user.finishLogin();
335
+ }
336
+ }
337
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <p class="text-gray-500 dark:text-gray-400 font-sm text-left mt-3">
3
+ {{$t('Have no account?')}}
4
+ <Link :to="`/signup${route.query.next ? `?next=${encodeURIComponent(route.query.next)}` : ''}`">
5
+ {{$t('sign up now')}}
6
+ </Link>
7
+ </p>
8
+ </template>
9
+
10
+ <script setup>
11
+ import Link from '@/afcl/Link.vue';
12
+ import { useRoute } from 'vue-router';
13
+
14
+ const route = useRoute();
15
+
16
+ </script>
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".", // This should point to your project root
4
+ "paths": {
5
+ "@/*": [
6
+ // "node_modules/adminforth/dist/spa/src/*"
7
+ "../../../spa/src/*"
8
+ ],
9
+ "*": [
10
+ // "node_modules/adminforth/dist/spa/node_modules/*"
11
+ "../../../spa/node_modules/*"
12
+ ],
13
+ "@@/*": [
14
+ // "node_modules/adminforth/dist/spa/src/*"
15
+ "."
16
+ ]
17
+ }
18
+ }
19
+ }
package/dist/index.js ADDED
@@ -0,0 +1,291 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
11
+ export default class OpenSignupPlugin extends AdminForthPlugin {
12
+ constructor(options) {
13
+ super(options, import.meta.url);
14
+ this.options = options;
15
+ }
16
+ modifyResourceConfig(adminforth, resourceConfig) {
17
+ const _super = Object.create(null, {
18
+ modifyResourceConfig: { get: () => super.modifyResourceConfig }
19
+ });
20
+ return __awaiter(this, void 0, void 0, function* () {
21
+ _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
22
+ if (!this.options.emailField) {
23
+ throw new Error(`emailField is required and should be a name of field in auth resource`);
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;
31
+ if (this.options.confirmEmails) {
32
+ if (!this.options.confirmEmails.adapter) {
33
+ throw new Error(`confirmEmails.adapter is required and should be a name of field in auth resource`);
34
+ }
35
+ if (!this.options.confirmEmails.emailConfirmedField) {
36
+ throw new Error(`confirmEmails.emailConfirmedField is required and should be a name of field in auth resource`);
37
+ }
38
+ const emailConfirmedField = this.authResource.columns.find(f => f.name === this.options.confirmEmails.emailConfirmedField);
39
+ if (!emailConfirmedField) {
40
+ const similar = suggestIfTypo(this.authResource.columns.map(f => f.name), this.options.confirmEmails.emailConfirmedField);
41
+ throw new Error(`Field with name ${this.options.confirmEmails.emailConfirmedField} not found in resource ${this.authResource.resourceId}.
42
+ ${similar ? `Did you mean ${similar}?` : ''}
43
+ `);
44
+ }
45
+ this.emailConfirmedField = emailConfirmedField;
46
+ }
47
+ const emailField = authResource.columns.find(f => f.name === this.options.emailField);
48
+ if (!emailField) {
49
+ const similar = suggestIfTypo(authResource.columns.map(f => f.name), this.options.emailField);
50
+ throw new Error(`Field with name ${this.options.emailField} not found in resource ${authResource.resourceId}.
51
+ ${similar ? `Did you mean ${similar}?` : ''}
52
+ `);
53
+ }
54
+ this.emailField = emailField;
55
+ if (!this.options.passwordField) {
56
+ throw new Error(`passwordField is required to get password constraints and should be a name of virtual field in auth resource`);
57
+ }
58
+ const passwordField = authResource.columns.find(f => f.name === this.options.passwordField);
59
+ if (!passwordField) {
60
+ const similar = suggestIfTypo(authResource.columns.map(f => f.name), this.options.passwordField);
61
+ throw new Error(`Field with name ${this.options.passwordField} not found in resource ${authResource.resourceId}.
62
+ ${similar ? `Did you mean ${similar}?` : ''}
63
+ `);
64
+ }
65
+ this.passwordField = passwordField;
66
+ adminforth.config.customization.loginPageInjections.underInputs.push({
67
+ file: this.componentPath('SignupUnderLogin.vue'),
68
+ });
69
+ adminforth.config.customization.customPages.push({
70
+ path: '/signup',
71
+ component: {
72
+ file: this.componentPath('SignupPage.vue'),
73
+ meta: {
74
+ customLayout: true,
75
+ pluginInstanceId: this.pluginInstanceId,
76
+ requestEmailConfirmation: !!this.options.confirmEmails
77
+ }
78
+ }
79
+ });
80
+ // for confirmation disable login if email is not confirmed
81
+ if (this.options.confirmEmails) {
82
+ if (!adminforth.config.auth.beforeLoginConfirmation) {
83
+ adminforth.config.auth.beforeLoginConfirmation = [];
84
+ }
85
+ }
86
+ });
87
+ }
88
+ validateConfigAfterDiscover(adminforth, resourceConfig) {
89
+ if (this.options.confirmEmails) {
90
+ this.options.confirmEmails.adapter.validate();
91
+ }
92
+ if (this.options.confirmEmails) {
93
+ const emailConfirmedColumn = this.resourceConfig.columns.find(f => f.name === this.options.confirmEmails.emailConfirmedField);
94
+ if (emailConfirmedColumn.type !== AdminForthDataTypes.BOOLEAN) {
95
+ throw new Error(`Field ${this.emailConfirmedField.name} must be of type boolean`);
96
+ }
97
+ }
98
+ }
99
+ instanceUniqueRepresentation(pluginOptions) {
100
+ // optional method to return unique string representation of plugin instance.
101
+ // Needed if plugin can have multiple instances on one resource
102
+ return `single`;
103
+ }
104
+ doLogin(email, response, extra) {
105
+ return __awaiter(this, void 0, void 0, function* () {
106
+ const username = email;
107
+ const userRecord = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
108
+ const adminUser = {
109
+ dbUser: userRecord,
110
+ pk: userRecord[this.authResource.columns.find((col) => col.primaryKey).name],
111
+ username,
112
+ };
113
+ const toReturn = { allowedLogin: true, error: '' };
114
+ yield this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response, extra);
115
+ if (toReturn.allowedLogin) {
116
+ this.adminforth.auth.setAuthCookie({
117
+ response,
118
+ username,
119
+ pk: userRecord[this.authResource.columns.find((col) => col.primaryKey).name]
120
+ });
121
+ }
122
+ return toReturn;
123
+ });
124
+ }
125
+ setupEndpoints(server) {
126
+ server.endpoint({
127
+ method: 'GET',
128
+ path: `/plugin/${this.pluginInstanceId}/password-constraints`,
129
+ noAuth: true,
130
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ tr }) {
131
+ return {
132
+ minLength: this.passwordField.minLength,
133
+ maxLength: this.passwordField.maxLength,
134
+ validation: yield Promise.all(this.passwordField.validation.map((_a) => __awaiter(this, [_a], void 0, function* ({ regExp, message }) { return ({ regExp, message: yield tr(message, 'opensignup') }); }))),
135
+ };
136
+ })
137
+ });
138
+ server.endpoint({
139
+ method: 'POST',
140
+ path: `/plugin/${this.pluginInstanceId}/complete-verified-signup`,
141
+ noAuth: true,
142
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, headers, query, cookies, tr, requestUrl }) {
143
+ const { token, password } = body;
144
+ const { email } = yield this.adminforth.auth.verify(token, 'tempVerifyEmailToken', false);
145
+ if (!email) {
146
+ return { error: 'Invalid token', ok: false };
147
+ }
148
+ if (!password) {
149
+ return { error: tr('Password is required', 'opensignup'), ok: false };
150
+ }
151
+ const userRecord = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
152
+ if (!userRecord) {
153
+ return { error: tr('User not found'), ok: false };
154
+ }
155
+ if (userRecord[this.options.confirmEmails.emailConfirmedField]) {
156
+ return { error: tr('Email already confirmed'), ok: false };
157
+ }
158
+ yield this.adminforth.resource(this.authResource.resourceId).update(userRecord[this.authResource.columns.find((col) => col.primaryKey).name], {
159
+ [this.options.confirmEmails.emailConfirmedField]: true,
160
+ [this.options.passwordHashField]: yield AdminForth.Utils.generatePasswordHash(password),
161
+ });
162
+ return yield this.doLogin(email, response, { body, headers, query, cookies, requestUrl });
163
+ })
164
+ });
165
+ server.endpoint({
166
+ method: 'POST',
167
+ path: `/plugin/${this.pluginInstanceId}/signup`,
168
+ noAuth: true,
169
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, headers, query, cookies, tr, requestUrl }) {
170
+ var _b, _c;
171
+ const { email, url, password } = body;
172
+ const extra = { body, headers, query, cookies, requestUrl: url };
173
+ // validate email
174
+ if (this.emailField.validation) {
175
+ for (const { regExp, message } of this.emailField.validation) {
176
+ if (!new RegExp(regExp).test(email)) {
177
+ return { error: yield tr(message, 'opensignup'), ok: false };
178
+ }
179
+ }
180
+ }
181
+ // validate password
182
+ if (!this.options.confirmEmails) {
183
+ if (password.length < this.passwordField.minLength) {
184
+ return {
185
+ error: yield tr(`Password must be at least ${this.passwordField.minLength} characters long`, 'opensignup'),
186
+ ok: false
187
+ };
188
+ }
189
+ if (password.length > this.passwordField.maxLength) {
190
+ return {
191
+ error: yield tr(`Password must be at most ${this.passwordField.maxLength} characters long`, 'opensignup'),
192
+ ok: false
193
+ };
194
+ }
195
+ if (this.passwordField.validation) {
196
+ for (const { regExp, message } of this.passwordField.validation) {
197
+ if (!new RegExp(regExp).test(password)) {
198
+ return { error: yield tr(message, 'opensignup'), ok: false };
199
+ }
200
+ }
201
+ }
202
+ }
203
+ // This is not needed when right email validator is set on email field because
204
+ // it will not allow to create such email, but if user forgot to set it it might save situation
205
+ const normalizedEmail = email.toLowerCase(); // normalize email
206
+ // first check again if email already exists
207
+ const existingUser = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, normalizedEmail));
208
+ if ((!this.options.confirmEmails && existingUser) || (this.options.confirmEmails && (existingUser === null || existingUser === void 0 ? void 0 : existingUser[this.emailConfirmedField.name]))) {
209
+ return { error: yield tr(`Email already exists`, 'opensignup'), ok: false };
210
+ }
211
+ // create user
212
+ if (!existingUser) {
213
+ if ((_b = this.options.hooks) === null || _b === void 0 ? void 0 : _b.beforeUserSave) {
214
+ const hook = this.options.hooks.beforeUserSave;
215
+ const recordToCreate = Object.assign(Object.assign(Object.assign({}, (this.options.defaultFieldValues || {})), (this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {})), { [this.emailField.name]: normalizedEmail, [this.options.passwordHashField]: password ? yield AdminForth.Utils.generatePasswordHash(password) : '' });
216
+ const resp = yield hook({
217
+ resource: this.authResource,
218
+ record: recordToCreate,
219
+ adminforth: this.adminforth,
220
+ extra,
221
+ });
222
+ if (!resp || (!resp.ok && !resp.error)) {
223
+ throw new Error(`Hook beforeUserSave must return object with {ok: true} or { error: 'Error' } `);
224
+ }
225
+ if (resp.error) {
226
+ return { error: resp.error };
227
+ }
228
+ }
229
+ 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]: normalizedEmail, [this.options.passwordHashField]: password ? yield AdminForth.Utils.generatePasswordHash(password) : '' }));
230
+ if ((_c = this.options.hooks) === null || _c === void 0 ? void 0 : _c.afterUserSave) {
231
+ const hook = this.options.hooks.afterUserSave;
232
+ const resp = yield hook({
233
+ resource: this.authResource,
234
+ record: created,
235
+ adminforth: this.adminforth,
236
+ extra,
237
+ });
238
+ if (!resp || (!resp.ok && !resp.error)) {
239
+ throw new Error(`Hook afterUserSave must return object with {ok: true} or { error: 'Error' } `);
240
+ }
241
+ if (resp.error) {
242
+ return { error: resp.error };
243
+ }
244
+ }
245
+ }
246
+ if (!this.options.confirmEmails) {
247
+ const resp = yield this.doLogin(email, response, { body, headers, query, cookies, requestUrl });
248
+ return resp;
249
+ }
250
+ // send confirmation email
251
+ const brandName = this.adminforth.config.customization.brandName;
252
+ const verifyToken = this.adminforth.auth.issueJWT({ email, issuer: brandName }, 'tempVerifyEmailToken', '2h');
253
+ process.env.HEAVY_DEBUG && console.log('🐛Sending reset tok to', verifyToken);
254
+ const emailText = tr(`
255
+ Dear user,
256
+ Welcome to {brandName}!
257
+
258
+ To confirm your email, click the link below:\n\n
259
+
260
+ {url}?verifyToken={verifyToken}\n\n
261
+
262
+ If you didn't request this, please ignore this email.\n\n
263
+ Link is valid for 2 hours.\n\n
264
+
265
+ Thanks,
266
+ The {brandName} Team
267
+
268
+ `, 'opensignup', { brandName, url, verifyToken });
269
+ const emailHtml = tr(`
270
+ <html>
271
+ <head></head>
272
+ <body>
273
+ <p>Dear user,</p>
274
+ <p>Welcome to {brandName}!</p>
275
+ <p>To confirm your email, click the link below:</p>
276
+ <a href="{url}?token={verifyToken}">Confirm email</a>
277
+ <p>If you didn't request this, please ignore this email.</p>
278
+ <p>Link is valid for 2 hours.</p>
279
+ <p>Thanks,</p>
280
+ <p>The {brandName} Team</p>
281
+ </body>
282
+ </html>
283
+ `, 'opensignup', { brandName, url, verifyToken });
284
+ const emailSubject = tr(`Signup request at {brandName}`, 'opensignup', { brandName });
285
+ // send email with AWS SES this.options.providerOptions.AWS_SES
286
+ this.options.confirmEmails.adapter.sendEmail(this.options.confirmEmails.sendFrom, email, emailText, emailHtml, emailSubject);
287
+ return { ok: true };
288
+ })
289
+ });
290
+ }
291
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};