@adminforth/open-signup 1.0.9 → 1.1.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,13 @@
1
+
2
+ #!/bin/bash
3
+
4
+ # write npm run output both to console and to build.log
5
+ npm run build 2>&1 | tee build.log
6
+ build_status=${PIPESTATUS[0]}
7
+
8
+ # if exist status from the npm run build is not 0
9
+ # then exit with the status code from the npm run build
10
+ if [ $build_status -ne 0 ]; then
11
+ echo "Build failed. Exiting with status code $build_status"
12
+ exit $build_status
13
+ fi
@@ -0,0 +1,44 @@
1
+ #!/bin/sh
2
+
3
+ set -x
4
+
5
+ COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
6
+
7
+
8
+ if [ "$CI_STEP_STATUS" = "success" ]; then
9
+ MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
10
+
11
+ curl -s -X POST -H "Content-Type: application/json" -d '{
12
+ "username": "'"$CI_COMMIT_AUTHOR"'",
13
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
14
+ "attachments": [
15
+ {
16
+ "mrkdwn_in": ["text", "pretext"],
17
+ "color": "#36a64f",
18
+ "text": "'"$MESSAGE"'"
19
+ }
20
+ ]
21
+ }' "$DEVELOPERS_SLACK_WEBHOOK"
22
+ exit 0
23
+ fi
24
+ export BUILD_LOG=$(cat ./build.log)
25
+
26
+ BUILD_LOG=$(echo $BUILD_LOG | sed 's/"/\\"/g')
27
+
28
+ MESSAGE="Broke \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\` with commit _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
29
+ CODE_BLOCK="\`\`\`$BUILD_LOG\n\`\`\`"
30
+
31
+ echo "Sending slack message to developers $MESSAGE"
32
+ # Send the message
33
+ curl -sS -X POST -H "Content-Type: application/json" -d '{
34
+ "username": "'"$CI_COMMIT_AUTHOR"'",
35
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
36
+ "attachments": [
37
+ {
38
+ "mrkdwn_in": ["text", "pretext"],
39
+ "color": "#8A1C12",
40
+ "text": "'"$CODE_BLOCK"'",
41
+ "pretext": "'"$MESSAGE"'"
42
+ }
43
+ ]
44
+ }' "$DEVELOPERS_SLACK_WEBHOOK" 2>&1
@@ -0,0 +1,42 @@
1
+ clone:
2
+ git:
3
+ image: woodpeckerci/plugin-git
4
+ settings:
5
+ partial: false
6
+ depth: 5
7
+
8
+ steps:
9
+ init-secrets:
10
+ when:
11
+ - event: push
12
+ image: infisical/cli
13
+ environment:
14
+ INFISICAL_TOKEN:
15
+ from_secret: VAULT_TOKEN
16
+ commands:
17
+ - infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
18
+ secrets:
19
+ - VAULT_TOKEN
20
+
21
+ release:
22
+ image: node:20
23
+ when:
24
+ - event: push
25
+ commands:
26
+ - apt update && apt install -y rsync
27
+ - export $(cat /woodpecker/deploy.vault.env | xargs)
28
+ - npm clean-install
29
+ - /bin/bash ./.woodpecker/buildRelease.sh
30
+ - npm audit signatures
31
+ - npx semantic-release
32
+
33
+ slack-on-failure:
34
+ when:
35
+ - event: push
36
+ status: [failure, success]
37
+ - event: push
38
+ image: curlimages/curl
39
+ commands:
40
+ - export $(cat /woodpecker/deploy.vault.env | xargs)
41
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh
42
+
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+
2
+ # Changelog
3
+
4
+ The complete changelog is available on the [GitHub Releases page](https://github.com/devforth/adminforth-open-signup/releases).
5
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Devforth.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/build.log ADDED
@@ -0,0 +1,12 @@
1
+
2
+ > @adminforth/open-signup@1.0.9 build
3
+ > tsc && rsync -av --exclude 'node_modules' custom dist/
4
+
5
+ sending incremental file list
6
+ custom/
7
+ custom/SignupPage.vue
8
+ custom/SignupUnderLogin.vue
9
+ custom/tsconfig.json
10
+
11
+ sent 14,444 bytes received 77 bytes 29,042.00 bytes/sec
12
+ total size is 14,155 speedup is 0.97
@@ -133,15 +133,16 @@
133
133
 
134
134
 
135
135
  <script setup lang="ts">
136
- import { onMounted, ref, computed, Ref, onBeforeMount } from 'vue';
136
+ import { onMounted, ref, computed, Ref, onBeforeMount, nextTick } from 'vue';
137
137
  import { useCoreStore } from '@/stores/core';
138
138
  import { useUserStore } from '@/stores/user';
139
139
  import { callAdminForthApi, loadFile, applyRegexValidation } from '@/utils';
140
- import { useRoute, useRouter } from 'vue-router';
140
+ import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
141
141
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
142
142
  import Button from '@/afcl/Button.vue';
143
143
  import Link from '@/afcl/Link.vue';
144
144
  import { useI18n } from 'vue-i18n';
145
+ import adminforth from '@/adminforth';
145
146
 
146
147
  const { t } = useI18n();
147
148
 
@@ -209,10 +210,6 @@ const validationError = computed(() => {
209
210
 
210
211
  const error: Ref<string | null> = ref(null);
211
212
 
212
- onMounted(async () => {
213
- emailInput.value?.focus();
214
- });
215
-
216
213
 
217
214
  const backgroundPosition = computed(() => {
218
215
  return coreStore.config?.loginBackgroundPosition || '1/2';
@@ -245,6 +242,7 @@ const passwordConstraints: Ref<{
245
242
  // }
246
243
 
247
244
  onBeforeMount(() => {
245
+ // console.log('⛔ SignUp Page. Before Mount: ', route)
248
246
  if (localStorage.getItem('isAuthorized') === 'true') {
249
247
  // if route has next param, redirect
250
248
  coreStore.fetchMenuAndResource();
@@ -257,15 +255,22 @@ onBeforeMount(() => {
257
255
  })
258
256
 
259
257
  onMounted(async () => {
258
+ emailInput.value?.focus();
259
+ await coreStore.getPublicConfig();
260
260
 
261
+ await nextTick();
262
+ await router.isReady();
261
263
 
262
- await coreStore.getPublicConfig();
263
264
  // getPasswordConstraints
265
+ console.log('⛔ SignUp Page. Before Mount: ', route.meta)
264
266
  passwordConstraints.value = await callAdminForthApi({
265
267
  path: `/plugin/${route.meta.pluginInstanceId}/password-constraints`,
266
268
  method: 'GET',
267
269
  });
268
- });
270
+ })
271
+
272
+ // onMounted(async () => {
273
+ // });
269
274
 
270
275
  async function doSignup() {
271
276
  error.value = null;
@@ -319,7 +324,7 @@ const signupAfterEmailConfirmation = async () => {
319
324
  }
320
325
  });
321
326
  if (resp.error) {
322
- window.adminforth.alert({
327
+ adminforth.alert({
323
328
  message: t(`Error fetching data: {error}`, { error: resp.error }),
324
329
  variant: 'danger',
325
330
  });
@@ -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
+ }
@@ -133,15 +133,16 @@
133
133
 
134
134
 
135
135
  <script setup lang="ts">
136
- import { onMounted, ref, computed, Ref, onBeforeMount } from 'vue';
136
+ import { onMounted, ref, computed, Ref, onBeforeMount, nextTick } from 'vue';
137
137
  import { useCoreStore } from '@/stores/core';
138
138
  import { useUserStore } from '@/stores/user';
139
139
  import { callAdminForthApi, loadFile, applyRegexValidation } from '@/utils';
140
- import { useRoute, useRouter } from 'vue-router';
140
+ import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
141
141
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
142
142
  import Button from '@/afcl/Button.vue';
143
143
  import Link from '@/afcl/Link.vue';
144
144
  import { useI18n } from 'vue-i18n';
145
+ import adminforth from '@/adminforth';
145
146
 
146
147
  const { t } = useI18n();
147
148
 
@@ -209,10 +210,6 @@ const validationError = computed(() => {
209
210
 
210
211
  const error: Ref<string | null> = ref(null);
211
212
 
212
- onMounted(async () => {
213
- emailInput.value?.focus();
214
- });
215
-
216
213
 
217
214
  const backgroundPosition = computed(() => {
218
215
  return coreStore.config?.loginBackgroundPosition || '1/2';
@@ -245,6 +242,7 @@ const passwordConstraints: Ref<{
245
242
  // }
246
243
 
247
244
  onBeforeMount(() => {
245
+ // console.log('⛔ SignUp Page. Before Mount: ', route)
248
246
  if (localStorage.getItem('isAuthorized') === 'true') {
249
247
  // if route has next param, redirect
250
248
  coreStore.fetchMenuAndResource();
@@ -257,15 +255,22 @@ onBeforeMount(() => {
257
255
  })
258
256
 
259
257
  onMounted(async () => {
258
+ emailInput.value?.focus();
259
+ await coreStore.getPublicConfig();
260
260
 
261
+ await nextTick();
262
+ await router.isReady();
261
263
 
262
- await coreStore.getPublicConfig();
263
264
  // getPasswordConstraints
265
+ console.log('⛔ SignUp Page. Before Mount: ', route.meta)
264
266
  passwordConstraints.value = await callAdminForthApi({
265
267
  path: `/plugin/${route.meta.pluginInstanceId}/password-constraints`,
266
268
  method: 'GET',
267
269
  });
268
- });
270
+ })
271
+
272
+ // onMounted(async () => {
273
+ // });
269
274
 
270
275
  async function doSignup() {
271
276
  error.value = null;
@@ -319,7 +324,7 @@ const signupAfterEmailConfirmation = async () => {
319
324
  }
320
325
  });
321
326
  if (resp.error) {
322
- window.adminforth.alert({
327
+ adminforth.alert({
323
328
  message: t(`Error fetching data: {error}`, { error: resp.error }),
324
329
  variant: 'danger',
325
330
  });
@@ -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 CHANGED
@@ -131,7 +131,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
131
131
  return {
132
132
  minLength: this.passwordField.minLength,
133
133
  maxLength: this.passwordField.maxLength,
134
- validation: yield Promise.all(this.passwordField.validation.map((_b) => __awaiter(this, [_b], void 0, function* ({ regExp, message }) { return ({ regExp, message: yield tr(message, 'opensignup') }); }))),
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
135
  };
136
136
  })
137
137
  });
@@ -139,7 +139,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
139
139
  method: 'POST',
140
140
  path: `/plugin/${this.pluginInstanceId}/complete-verified-signup`,
141
141
  noAuth: true,
142
- handler: (_c) => __awaiter(this, [_c], void 0, function* ({ body, response, headers, query, cookies, tr }) {
142
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, headers, query, cookies, tr, requestUrl }) {
143
143
  const { token, password } = body;
144
144
  const { email } = yield this.adminforth.auth.verify(token, 'tempVerifyEmailToken', false);
145
145
  if (!email) {
@@ -159,20 +159,22 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
159
159
  [this.options.confirmEmails.emailConfirmedField]: true,
160
160
  [this.options.passwordHashField]: yield AdminForth.Utils.generatePasswordHash(password),
161
161
  });
162
- return yield this.doLogin(email, response, { body, headers, query, cookies });
162
+ return yield this.doLogin(email, response, { body, headers, query, cookies, requestUrl });
163
163
  })
164
164
  });
165
165
  server.endpoint({
166
166
  method: 'POST',
167
167
  path: `/plugin/${this.pluginInstanceId}/signup`,
168
168
  noAuth: true,
169
- handler: (_d) => __awaiter(this, [_d], void 0, function* ({ body, response, headers, query, cookies, tr }) {
169
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, headers, query, cookies, tr, requestUrl }) {
170
+ var _b, _c;
170
171
  const { email, url, password } = body;
172
+ const extra = { body, headers, query, cookies, requestUrl: url };
171
173
  // validate email
172
174
  if (this.emailField.validation) {
173
175
  for (const { regExp, message } of this.emailField.validation) {
174
176
  if (!new RegExp(regExp).test(email)) {
175
- return { error: message, ok: false };
177
+ return { error: yield tr(message, 'opensignup'), ok: false };
176
178
  }
177
179
  }
178
180
  }
@@ -180,20 +182,20 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
180
182
  if (!this.options.confirmEmails) {
181
183
  if (password.length < this.passwordField.minLength) {
182
184
  return {
183
- error: tr(`Password must be at least ${this.passwordField.minLength} characters long`, 'opensignup'),
185
+ error: yield tr(`Password must be at least ${this.passwordField.minLength} characters long`, 'opensignup'),
184
186
  ok: false
185
187
  };
186
188
  }
187
189
  if (password.length > this.passwordField.maxLength) {
188
190
  return {
189
- error: tr(`Password must be at most ${this.passwordField.maxLength} characters long`, 'opensignup'),
191
+ error: yield tr(`Password must be at most ${this.passwordField.maxLength} characters long`, 'opensignup'),
190
192
  ok: false
191
193
  };
192
194
  }
193
195
  if (this.passwordField.validation) {
194
196
  for (const { regExp, message } of this.passwordField.validation) {
195
197
  if (!new RegExp(regExp).test(password)) {
196
- return { error: tr(message, 'opensignup'), ok: false };
198
+ return { error: yield tr(message, 'opensignup'), ok: false };
197
199
  }
198
200
  }
199
201
  }
@@ -204,14 +206,45 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
204
206
  // first check again if email already exists
205
207
  const existingUser = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, normalizedEmail));
206
208
  if ((!this.options.confirmEmails && existingUser) || (this.options.confirmEmails && (existingUser === null || existingUser === void 0 ? void 0 : existingUser[this.emailConfirmedField.name]))) {
207
- return { error: tr('Email already exists', 'opensignup'), ok: false };
209
+ return { error: yield tr(`Email already exists`, 'opensignup'), ok: false };
208
210
  }
209
211
  // create user
210
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
+ }
211
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
+ }
212
245
  }
213
246
  if (!this.options.confirmEmails) {
214
- const resp = yield this.doLogin(email, response, { body, headers, query, cookies });
247
+ const resp = yield this.doLogin(email, response, { body, headers, query, cookies, requestUrl });
215
248
  return resp;
216
249
  }
217
250
  // send confirmation email
package/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
2
2
  import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, HttpExtra } from "adminforth";
3
3
  import type { PluginOptions } from './types.js';
4
- import { error } from "console";
5
4
 
6
5
 
7
6
  export default class OpenSignupPlugin extends AdminForthPlugin {
@@ -162,7 +161,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
162
161
  method: 'POST',
163
162
  path: `/plugin/${this.pluginInstanceId}/complete-verified-signup`,
164
163
  noAuth: true,
165
- handler: async ({ body, response, headers, query, cookies, tr }) => {
164
+ handler: async ({ body, response, headers, query, cookies, tr, requestUrl }) => {
166
165
  const { token, password } = body;
167
166
  const { email } = await this.adminforth.auth.verify(token, 'tempVerifyEmailToken', false);
168
167
  if (!email) {
@@ -185,7 +184,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
185
184
  [this.options.confirmEmails.emailConfirmedField]: true,
186
185
  [this.options.passwordHashField]: await AdminForth.Utils.generatePasswordHash(password),
187
186
  });
188
- return await this.doLogin(email, response, { body, headers, query, cookies });
187
+ return await this.doLogin(email, response, { body, headers, query, cookies, requestUrl });
189
188
  }
190
189
  });
191
190
 
@@ -193,36 +192,35 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
193
192
  method: 'POST',
194
193
  path: `/plugin/${this.pluginInstanceId}/signup`,
195
194
  noAuth: true,
196
- handler: async ({ body, response, headers, query, cookies, tr }) => {
195
+ handler: async ({ body, response, headers, query, cookies, tr, requestUrl }) => {
197
196
  const { email, url, password } = body;
198
-
197
+ const extra = { body, headers, query, cookies, requestUrl: url };
199
198
  // validate email
200
199
  if (this.emailField.validation) {
201
200
  for (const { regExp, message } of this.emailField.validation) {
202
201
  if (!new RegExp(regExp).test(email)) {
203
- return { error: message, ok: false };
202
+ return { error: await tr(message, 'opensignup'), ok: false };
204
203
  }
205
204
  }
206
205
  }
207
-
208
206
  // validate password
209
207
  if (!this.options.confirmEmails) {
210
208
  if (password.length < this.passwordField.minLength) {
211
209
  return {
212
- error: tr(`Password must be at least ${this.passwordField.minLength} characters long`, 'opensignup'),
210
+ error: await tr(`Password must be at least ${this.passwordField.minLength} characters long`, 'opensignup'),
213
211
  ok: false
214
212
  };
215
213
  }
216
214
  if (password.length > this.passwordField.maxLength) {
217
215
  return {
218
- error: tr(`Password must be at most ${this.passwordField.maxLength} characters long`, 'opensignup'),
216
+ error: await tr(`Password must be at most ${this.passwordField.maxLength} characters long`, 'opensignup'),
219
217
  ok: false
220
218
  };
221
219
  }
222
220
  if (this.passwordField.validation) {
223
221
  for (const { regExp, message } of this.passwordField.validation) {
224
222
  if (!new RegExp(regExp).test(password)) {
225
- return { error: tr(message, 'opensignup'), ok: false };
223
+ return { error: await tr(message, 'opensignup'), ok: false };
226
224
  }
227
225
  }
228
226
  }
@@ -235,21 +233,60 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
235
233
  // first check again if email already exists
236
234
  const existingUser = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, normalizedEmail));
237
235
  if ((!this.options.confirmEmails && existingUser) || (this.options.confirmEmails && existingUser?.[this.emailConfirmedField.name])) {
238
- return { error: tr('Email already exists', 'opensignup'), ok: false };
236
+ return { error: await tr(`Email already exists`, 'opensignup'), ok: false };
239
237
  }
240
238
 
241
239
  // create user
242
240
  if (!existingUser) {
241
+ if (this.options.hooks?.beforeUserSave) {
242
+ const hook = this.options.hooks.beforeUserSave;
243
+ const recordToCreate = {
244
+ ...(this.options.defaultFieldValues || {}),
245
+ ...(this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {}),
246
+ [this.emailField.name]: normalizedEmail,
247
+ [this.options.passwordHashField]: password ? await AdminForth.Utils.generatePasswordHash(password) : '',
248
+ };
249
+ const resp = await hook({
250
+ resource: this.authResource,
251
+ record: recordToCreate,
252
+ adminforth: this.adminforth,
253
+ extra,
254
+ });
255
+
256
+ if (!resp || (!resp.ok && !resp.error)) {
257
+ throw new Error(`Hook beforeUserSave must return object with {ok: true} or { error: 'Error' } `);
258
+ }
259
+ if (resp.error) {
260
+ return { error: resp.error };
261
+ }
262
+ }
243
263
  const created = await this.adminforth.resource(this.authResource.resourceId).create({
244
264
  ...(this.options.defaultFieldValues || {}),
245
265
  ...(this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {}),
246
266
  [this.emailField.name]: normalizedEmail,
247
267
  [this.options.passwordHashField]: password ? await AdminForth.Utils.generatePasswordHash(password) : '',
248
268
  });
269
+
270
+ if (this.options.hooks?.afterUserSave) {
271
+ const hook = this.options.hooks.afterUserSave;
272
+ const resp = await hook({
273
+ resource: this.authResource,
274
+ record: created,
275
+ adminforth: this.adminforth,
276
+ extra,
277
+ });
278
+
279
+ if (!resp || (!resp.ok && !resp.error)) {
280
+ throw new Error(`Hook afterUserSave must return object with {ok: true} or { error: 'Error' } `);
281
+ }
282
+ if (resp.error) {
283
+ return { error: resp.error };
284
+ }
285
+ }
249
286
  }
250
287
 
251
288
  if (!this.options.confirmEmails) {
252
- const resp = await this.doLogin(email, response, { body, headers, query, cookies });
289
+ const resp = await this.doLogin(email, response, { body, headers, query, cookies, requestUrl });
253
290
  return resp;
254
291
  }
255
292
 
package/package.json CHANGED
@@ -1,19 +1,55 @@
1
1
  {
2
2
  "name": "@adminforth/open-signup",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "build": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch",
9
- "rollout": "npm run build && npm publish --access public",
8
+ "build": "tsc && rsync -av --exclude 'node_modules' custom dist/",
10
9
  "prepare": "npm link adminforth"
11
10
  },
12
- "keywords": [],
13
- "author": "",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/devforth/adminforth-open-signup.git"
14
+ },
15
+ "keywords": [
16
+ "adminforth",
17
+ "signup"
18
+ ],
19
+ "author": "devforth",
14
20
  "license": "ISC",
15
- "description": "",
21
+ "description": "Open signup for adminforth",
16
22
  "dependencies": {
17
23
  "@aws-sdk/client-ses": "^3.654.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.7",
27
+ "semantic-release": "^24.2.1",
28
+ "semantic-release-slack-bot": "^4.0.2",
29
+ "typescript": "^5.7.3"
30
+ },
31
+ "release": {
32
+ "plugins": [
33
+ "@semantic-release/commit-analyzer",
34
+ "@semantic-release/release-notes-generator",
35
+ "@semantic-release/npm",
36
+ "@semantic-release/github",
37
+ [
38
+ "semantic-release-slack-bot",
39
+ {
40
+ "notifyOnSuccess": true,
41
+ "notifyOnFail": true,
42
+ "slackIcon": ":package:",
43
+ "markdownReleaseNotes": true
44
+ }
45
+ ]
46
+ ],
47
+ "branches": [
48
+ "main",
49
+ {
50
+ "name": "next",
51
+ "prerelease": true
52
+ }
53
+ ]
18
54
  }
19
55
  }
package/types.ts CHANGED
@@ -54,6 +54,15 @@ export interface PluginOptions {
54
54
  */
55
55
  adapter: EmailAdapter;
56
56
  }
57
+
58
+
59
+ /**
60
+ * Hooks to be executed before and after user is created
61
+ */
62
+ hooks?: {
63
+ beforeUserSave?: any;
64
+ afterUserSave?: any;
65
+ }
57
66
 
58
67
 
59
68
  }
package/Changelog.md DELETED
@@ -1,15 +0,0 @@
1
- ## [v1.0.9]
2
-
3
- # Fixed
4
- - when user created normalize email to lowercase. This is not needed when right email validator is set on email field because
5
- it will not allow to create such email, but if user forgot to set it it might save situation
6
-
7
- ## [v1.0.8]
8
-
9
- # Fixed
10
- - ensure next params keeps be encoded as URI between moving to/from login page
11
-
12
- ## [v1.0.7]
13
-
14
- # Added
15
- - next param support
@@ -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>