@sowell/quasar-app-extension-auth 0.2.4 → 0.2.8

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,24 @@
1
+ name: npm-publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ npm-publish:
10
+ name: npm-publish
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v2
15
+ - name: Nodejs
16
+ uses: actions/setup-node@v2
17
+ with:
18
+ node-version: "14"
19
+ cache: yarn
20
+ registry-url: https://registry.npmjs.org
21
+ - name: publish
22
+ run: npm publish --access public
23
+ env:
24
+ NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  # Install
9
9
 
10
10
  ```bash
11
- quasar ext add mamy-auth
11
+ quasar ext add @sowell/auth
12
12
  ```
13
13
 
14
14
  Quasar CLI will retrieve it from NPM and install the extension.
@@ -39,7 +39,7 @@ The default is http://localhost:3000, you must point it to your server base url
39
39
 
40
40
  6. Server route for authentication
41
41
 
42
- The default is /users/sign_in
42
+ The default is /auth
43
43
 
44
44
  7. Server route to reset password
45
45
 
@@ -78,7 +78,7 @@ The default is auth/getInfosUser and return email or phone number
78
78
  # Uninstall
79
79
 
80
80
  ```bash
81
- quasar ext remove mamy-auth
81
+ quasar ext remove @sowell/auth
82
82
  ```
83
83
 
84
84
  # Info
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sowell/quasar-app-extension-auth",
3
- "version": "0.2.4",
3
+ "version": "0.2.8",
4
4
  "description": "A Quasar app extension to manage Authentication",
5
5
  "main": "src/index.js",
6
6
  "repository": "https://github.com/sowell-ai/quasar.extension.auth",
@@ -0,0 +1,5 @@
1
+ <svg id="noun_Error_1688921" xmlns="http://www.w3.org/2000/svg" width="63" height="63" viewBox="0 0 63 63">
2
+ <circle id="Ellipse_90" data-name="Ellipse 90" cx="31.5" cy="31.5" r="31.5" fill="#f2d7d7"/>
3
+ <path id="Tracé_727" data-name="Tracé 727" d="M14.477,29.163h0A4.078,4.078,0,0,1,10.5,25.186V9.277A4.078,4.078,0,0,1,14.477,5.3h0a4.078,4.078,0,0,1,3.977,3.977V25.186A4.078,4.078,0,0,1,14.477,29.163Z" transform="translate(17.34 8.753)" fill="#ce0101"/>
4
+ <path id="Tracé_728" data-name="Tracé 728" d="M14.477,23.654h0A4.078,4.078,0,0,1,10.5,19.677h0A4.078,4.078,0,0,1,14.477,15.7h0a4.078,4.078,0,0,1,3.977,3.977h0A4.078,4.078,0,0,1,14.477,23.654Z" transform="translate(17.34 25.927)" fill="#ce0101"/>
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="62" height="62" viewBox="0 0 62 62">
2
+ <g id="noun_validation_1683882" transform="translate(0.1 -0.4)">
3
+ <circle id="Ellipse_89" data-name="Ellipse 89" cx="31" cy="31" r="31" transform="translate(-0.1 0.4)" fill="#dff2d6"/>
4
+ <path id="Tracé_724" data-name="Tracé 724" d="M36.165,13.221a1.189,1.189,0,0,0-1.868.311L21.84,31.906l-8.253-8.253a1.169,1.169,0,0,0-1.868.156,1.667,1.667,0,0,0,.156,2.18l9.5,9.342h0c.156.156.156.156.311.156a.152.152,0,0,1,.156.156c.156,0,.311.156.623.156h0c.156,0,.467,0,.467-.156l.156-.156c.156,0,.156-.156.311-.156h0L36.632,15.713A2.752,2.752,0,0,0,36.165,13.221Z" transform="translate(6.399 6.953)" fill="#3b9843"/>
5
+ </g>
6
+ </svg>
@@ -12,8 +12,8 @@ export default boot(({ router, store }: BootInterface) => {
12
12
  let isValid = store.getters["token/getIsValid"];
13
13
  let token = store.getters["token/getRaw"];
14
14
  if (
15
- !isValid &&
16
- !token &&
15
+ (!isValid ||
16
+ !token ) &&
17
17
  to.path !== LOCAL_LOGIN_ROUTE &&
18
18
  to.name !== "update_password"
19
19
  ) {
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <q-card style="min-width: 350px; border-radius: 30px">
3
+ <q-toolbar>
4
+ <q-space />
5
+ <q-btn
6
+ class="q-pt-md"
7
+ flat
8
+ dense
9
+ color="primary"
10
+ icon="close"
11
+ v-close-popup
12
+ />
13
+ </q-toolbar>
14
+ <q-card-section class="text-center">
15
+ <img v-if="sendEmailSuccess" src="../../assets/success.svg" />
16
+ <img v-else src="../../assets/error.svg" />
17
+ </q-card-section>
18
+
19
+ <q-card-section class="q-pt-none q-pl-lg q-pr-lg">
20
+ <div
21
+ class="text-subtitle1 q-mt-sm text-center"
22
+ style="font-size: 14px; font-weigth: 300; line-height: 20px"
23
+ data-cy="resetMessage"
24
+ >
25
+ {{
26
+ sendEmailSuccess ? $t("auth.emailSent") : $t("auth.emailNotRecognize")
27
+ }}
28
+ </div>
29
+ <div
30
+ class="q-mt-sm text-center q-mb-xl"
31
+ style="font-size: 15px; color: #737676"
32
+ v-if="sendEmailSuccess"
33
+ >
34
+ {{ emailResetPassword }}
35
+ </div>
36
+ </q-card-section>
37
+ <q-card-actions class="q-ml-lg q-mr-lg q-mb-lg">
38
+ <q-btn
39
+ v-if="!sendEmailSuccess"
40
+ data-cy="submit-login"
41
+ rounded
42
+ :label="$t('auth.reSend')"
43
+ color="primary"
44
+ padding="10px 5px 10px 5px"
45
+ :loading="loading"
46
+ no-caps
47
+ type="submit"
48
+ @click="reSendEmail"
49
+ class="full-width"
50
+ />
51
+ </q-card-actions>
52
+ </q-card>
53
+ </template>
54
+
55
+ <script lang="ts">
56
+ import { defineComponent } from "vue";
57
+
58
+ export default defineComponent({
59
+ props: {
60
+ emailResetPassword: {
61
+ type: String,
62
+ required: true,
63
+ },
64
+ sendEmailSuccess: {
65
+ type: Boolean,
66
+ required: true,
67
+ },
68
+ loading: {
69
+ type: Boolean,
70
+ required: true,
71
+ },
72
+ },
73
+ emit: ["resetPassword"],
74
+ setup(props: any, { emit }: any) {
75
+ const reSendEmail = () => {
76
+ emit("reSendEmail");
77
+ };
78
+ return { reSendEmail };
79
+ },
80
+ });
81
+ </script>
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <q-card style="min-width: 350px; border-radius: 30px">
3
+ <q-toolbar>
4
+ <q-space />
5
+ <q-btn
6
+ class="q-pt-md"
7
+ flat
8
+ dense
9
+ color="primary"
10
+ icon="close"
11
+ v-close-popup
12
+ />
13
+ </q-toolbar>
14
+ <q-card-section>
15
+ <p
16
+ class="resetPasswordTitle text-center"
17
+ style="font-size: 20px; font-weigth: 600"
18
+ >
19
+ {{ $t("auth.tapEmail") }}
20
+ </p>
21
+ </q-card-section>
22
+
23
+ <q-card-section class="q-pt-none q-pl-lg q-pr-lg">
24
+ <q-input
25
+ v-model="email"
26
+ dense
27
+ outlined
28
+ data-cy="emailReset"
29
+ :placeholder="$t('auth.email')"
30
+ autofocus
31
+ />
32
+ <div
33
+ class="text-subtitle1 q-mt-sm"
34
+ style="font-size: 13px; font-weigth: 300; line-height: 20px"
35
+ >
36
+ {{ $t("auth.resetText") }}
37
+ </div>
38
+ </q-card-section>
39
+
40
+ <q-card-actions align="center" class="q-ml-lg q-mr-lg q-mb-lg">
41
+ <q-btn
42
+ rounded
43
+ color="primary"
44
+ data-cy="sendEmails"
45
+ no-caps
46
+ :label="$t('common.reset')"
47
+ class="btn-min full-width"
48
+ @click="sendEmail"
49
+ v-close-popup
50
+ />
51
+ </q-card-actions>
52
+ </q-card>
53
+ </template>
54
+
55
+ <script lang="ts">
56
+ // @ts-ignore
57
+ import { defineComponent, ref } from "vue";
58
+
59
+ export default defineComponent({
60
+ emit: ["resetPassword"],
61
+ setup(props: any, { emit }: any) {
62
+ const email = ref("");
63
+ const sendEmail = () => {
64
+ emit("resetPassword", email.value);
65
+ };
66
+ return { sendEmail, email };
67
+ },
68
+ });
69
+ </script>
@@ -43,6 +43,7 @@
43
43
  <div
44
44
  class="q-mb-xl"
45
45
  @click="forgottenPassword = true"
46
+ data-cy="forgot-password"
46
47
  style="color: #40c4ff; font-weight: 500; cursor: pointer"
47
48
  >
48
49
  {{ $t("auth.forgottenPassword") }}
@@ -63,41 +64,15 @@
63
64
  </div>
64
65
  <div>
65
66
  <q-dialog v-model="forgottenPassword" persistent>
66
- <q-card style="min-width: 350px">
67
- <q-card-section>
68
- <p class="resetPasswordTitle">
69
- {{ $t("auth.tapEmail") }}
70
- </p>
71
- </q-card-section>
72
-
73
- <q-card-section class="q-pt-none">
74
- <q-input
75
- v-model="emailResetPassword"
76
- dense
77
- rounded
78
- outlined
79
- :placeholder="$t('auth.email')"
80
- autofocus
81
- />
82
- </q-card-section>
83
-
84
- <q-card-actions align="right">
85
- <q-btn
86
- outline
87
- rounded
88
- :label="$t('common.cancel')"
89
- class="btn-min"
90
- v-close-popup
91
- />
92
- <q-btn
93
- rounded
94
- color="primary"
95
- :label="$t('common.send')"
96
- class="btn-min"
97
- @click="resetPassword"
98
- />
99
- </q-card-actions>
100
- </q-card>
67
+ <send-email @resetPassword="resetPassword" />
68
+ </q-dialog>
69
+ <q-dialog v-model="isReset" persistent>
70
+ <sowell-message
71
+ :emailResetPassword="emailResetPassword"
72
+ :sendEmailSuccess="sendEmailSuccess"
73
+ :loading="loading"
74
+ @reSendEmail="resetPassword"
75
+ />
101
76
  </q-dialog>
102
77
  </div>
103
78
  </q-page>
@@ -105,7 +80,6 @@
105
80
  </template>
106
81
 
107
82
  <script lang="ts">
108
- //import prompts from "app/quasar.extensions.json";
109
83
  import isEmail from "validator/es/lib/isEmail";
110
84
  import { defineComponent, ref, onMounted } from "vue";
111
85
  import { $api, $prompts, $router, $store } from "../../injects";
@@ -113,6 +87,8 @@ import { useQuasar } from "quasar";
113
87
  import { useI18n } from "vue-i18n";
114
88
  import { AxiosError, AxiosResponse } from "axios";
115
89
  import prompts from "app/quasar.extensions.json";
90
+ import SendEmail from "../dialogs/SendEmail.vue";
91
+ import SowellMessage from "../dialogs/Message.vue";
116
92
 
117
93
  interface HeadersInterface {
118
94
  headers: {
@@ -122,6 +98,7 @@ interface HeadersInterface {
122
98
 
123
99
  export default defineComponent({
124
100
  name: "Auth",
101
+ components: { SendEmail, SowellMessage },
125
102
  setup() {
126
103
  const {
127
104
  "@sowell/auth": { X_REQUESTED_ROLES },
@@ -131,12 +108,15 @@ export default defineComponent({
131
108
  const loading = ref(false);
132
109
  const showPassword = ref(false);
133
110
  const forgottenPassword = ref(false);
111
+ const isReset = ref(false);
112
+ const sendEmailSuccess = ref(false);
134
113
  const emailResetPassword = ref("");
135
114
  const { post } = $api();
136
115
  const {
137
116
  LOCAL_SUCCESS_REDIRECTION_ROUTE,
138
117
  AUTH_SERVER_SIGNING_ROUTE,
139
118
  AUTH_SERVER_RESET_PASSWORD_ROUTE,
119
+ APP_SERVER_BASE_URL,
140
120
  } = $prompts();
141
121
  const $q = useQuasar();
142
122
  const { push, replace } = $router();
@@ -157,26 +137,23 @@ export default defineComponent({
157
137
  password: [(val: string) => !!val || t("auth.passwordPresenceError")],
158
138
  });
159
139
 
160
- const resetPassword = () => {
140
+ const resetPassword = (email: string) => {
161
141
  const data = {
162
- email: emailResetPassword.value,
142
+ auth: {
143
+ email,
144
+ base: APP_SERVER_BASE_URL,
145
+ },
163
146
  };
164
147
 
148
+
165
149
  post(AUTH_SERVER_RESET_PASSWORD_ROUTE, data)
166
150
  .then(() => {
167
- $q.notify({
168
- message: t("auth.emailSent"),
169
- color: "positive",
170
- position: "top",
171
- });
172
151
  forgottenPassword.value = false;
152
+ isReset.value = true;
153
+ sendEmailSuccess.value = true;
173
154
  })
174
155
  .catch(() => {
175
- $q.notify({
176
- message: t("auth.emailNotRecognize"),
177
- color: "negative",
178
- position: "top",
179
- });
156
+ isReset.value = true;
180
157
  });
181
158
  };
182
159
 
@@ -251,6 +228,8 @@ export default defineComponent({
251
228
  forgottenPassword,
252
229
  emailResetPassword,
253
230
  resetPassword,
231
+ isReset,
232
+ sendEmailSuccess,
254
233
  };
255
234
  },
256
235
  });
@@ -41,7 +41,7 @@
41
41
  no-caps
42
42
  data-cy="send-new-password"
43
43
  class="full-width"
44
- :disable="canSubmit"
44
+ :disable="!canSubmit"
45
45
  />
46
46
  </div>
47
47
  </q-form>
@@ -52,50 +52,111 @@
52
52
 
53
53
  <script lang="ts">
54
54
  //@ts-ignore
55
- import { computed, defineComponent, ref } from "vue";
55
+ import { computed, defineComponent, ref, Ref, onMounted } from "vue";
56
56
  import { useQuasar } from "quasar";
57
57
  import { useI18n } from "vue-i18n";
58
- import { useRouter } from "vue-router";
59
- import { $api, $prompts } from "../../injects";
58
+ import { $api, $prompts, $router } from "../../injects";
59
+ import jwt from 'jwt-decode'
60
+ import { Buffer } from 'buffer'
61
+
62
+ interface DecodedToken { // FIXME: consider to move to its own file
63
+ sub: number,
64
+ email: string,
65
+ token: string
66
+ }
60
67
 
61
68
  export default defineComponent({
62
69
  name: "ResetPassword",
63
70
 
64
71
  setup() {
65
- const { patch } = $api();
72
+ const { patch, post } = $api();
66
73
  const {
74
+ AUTH_SERVER_SIGNING_ROUTE,
67
75
  AUTH_SERVER_UPDATE_PASSWORD_ROUTE,
68
- LOCAL_SUCCESS_REDIRECTION_ROUTE,
76
+ LOCAL_LOGIN_ROUTE,
69
77
  } = $prompts();
70
78
  const $q = useQuasar();
71
- const router = useRouter();
79
+ const router = $router()
80
+ const $route = router.currentRoute.value
72
81
  const password = ref("");
73
82
  const loading = ref(false);
74
83
  const { t } = useI18n();
75
84
 
76
- const canSubmit = computed(() => password.value.length < 6);
85
+ const token = ref("")
86
+
87
+ const decodedToken = computed(() => {
88
+ if (!token.value) return null
89
+ return jwt(token.value) as DecodedToken
90
+ }) as Ref<DecodedToken | null>
91
+
92
+ const canSubmit = computed(() => {
93
+ return decodedToken.value !== null && password.value.length >= 6
94
+ });
77
95
 
78
96
  const validations = {
79
97
  passwordRules: [
80
98
  (val: string) => !!val || t("auth.passwordPresenceError"),
81
- (val: string) => val.length >= 6 || t("auth.passwordLengthError"),
99
+ (val: string) => val.length >= 6 || t("auth.passwordLengthError")
100
+ ],
101
+ passwordConfirmationRules: [
102
+ (val: string) =>
103
+ val === password.value || t("auth.newPasswordConfirmationFailed"),
104
+ (val: string) => !!val || t("auth.resetPasswordPresenceError"),
105
+ (val: string) => val.length >= 6 || t("auth.resetPasswordLengthError"),
82
106
  ],
83
107
  };
84
108
 
109
+ const decodeKey = (key: string) => {
110
+ try {
111
+ const decodedKey = Buffer.from(key, "base64").toString("utf-8").split(',')
112
+ if (decodedKey.length !== 2) {
113
+ throw 'Invalid encoded key'
114
+ }
115
+ console.log('decodedKey', decodedKey)
116
+ const email = decodedKey[0].replace('{', '').split(':')[1]
117
+ const tmpToken = decodedKey[1].replace('}', '').split(':')[1]
118
+ return {
119
+ decoded: true,
120
+ email,
121
+ tmpToken
122
+ }
123
+ } catch (err) {
124
+ console.log('decodeKey err', err)
125
+ return {
126
+ decoded: false
127
+ }
128
+ }
129
+ }
130
+
85
131
  const onSubmit = () => {
86
- const data = {
87
- password: password.value,
88
- };
89
- patch(AUTH_SERVER_UPDATE_PASSWORD_ROUTE, data)
90
- .then(() => {
132
+ const updatePasswordPayload = {
133
+ data: {
134
+ type: "users",
135
+ id: decodedToken.value.sub,
136
+ attributes:
137
+ {
138
+ password: password.value
139
+ }
140
+ }
141
+ }
142
+ const updatePasswordHeaders = {
143
+ headers: {
144
+ 'Authorization': `Bearer ${token.value}`
145
+ }
146
+ }
147
+
148
+ patch(
149
+ AUTH_SERVER_UPDATE_PASSWORD_ROUTE.replace(':userId', String(decodedToken.value.sub)),
150
+ updatePasswordPayload,
151
+ updatePasswordHeaders
152
+ ).then(() => {
91
153
  $q.notify({
92
154
  message: t("auth.passwordRecoverySuccess"),
93
155
  color: "positive",
94
156
  position: "top",
95
157
  });
96
- router.push(LOCAL_SUCCESS_REDIRECTION_ROUTE);
97
- })
98
- .catch(() => {
158
+ router.push(LOCAL_LOGIN_ROUTE);
159
+ }).catch(() => {
99
160
  $q.notify({
100
161
  message: t("auth.passwordRecoveryFailed"),
101
162
  color: "negative",
@@ -104,6 +165,49 @@ export default defineComponent({
104
165
  });
105
166
  };
106
167
 
168
+ onMounted(() => {
169
+ let key = $route.query.key as string
170
+ const { decoded, email, tmpToken } = decodeKey(key)
171
+
172
+ if (!decoded) {
173
+ return $q.notify({
174
+ message: t("auth.passwordRecoveryInvalidLink"),
175
+ color: "negative",
176
+ position: "top",
177
+ })
178
+ }
179
+
180
+ const tmpAuthPayload = {
181
+ auth: {
182
+ email: email,
183
+ token: tmpToken
184
+ }
185
+ }
186
+
187
+ post(AUTH_SERVER_SIGNING_ROUTE, tmpAuthPayload)
188
+ .then((response) => {
189
+ token.value = response.data.jwt
190
+ if (!token.value) throw 'Error reading the temporary token'
191
+ })
192
+ .catch((error) => {
193
+ if (error.response) {
194
+ // Request made and server responded
195
+ if (error.response.status === 404) {
196
+ return $q.notify({
197
+ message: t("auth.passwordRecoveryExpiredLink"),
198
+ color: "negative",
199
+ position: "top",
200
+ });
201
+ }
202
+ }
203
+ return $q.notify({
204
+ message: t("auth.passwordRecoveryFailed"),
205
+ color: "negative",
206
+ position: "top",
207
+ });
208
+ });
209
+ })
210
+
107
211
  return {
108
212
  password,
109
213
  validations,
@@ -1,5 +1,6 @@
1
1
  import { inject } from "vue";
2
2
  export interface PromptsInterface {
3
+ APP_SERVER_BASE_URL: string;
3
4
  LOCAL_LOGIN_ROUTE: string;
4
5
  LOCAL_SUCCESS_REDIRECTION_ROUTE: string;
5
6
  LOCAL_CHECK_CODE_ROUTE: string;
package/src/prompts.js CHANGED
@@ -27,7 +27,7 @@ module.exports = function () {
27
27
  type: "input",
28
28
  required: true,
29
29
  message: "Route to update the password (page)",
30
- default: "/reset_password",
30
+ default: "/ResetPassword/",
31
31
  },
32
32
  {
33
33
  name: "AUTH_SERVER_BASE_URL",
@@ -41,7 +41,7 @@ module.exports = function () {
41
41
  type: "input",
42
42
  required: true,
43
43
  message: "API endpoint for signin (without host)",
44
- default: "/users/sign_in",
44
+ default: "/auth",
45
45
  },
46
46
  {
47
47
  name: "AUTH_SERVER_RESET_PASSWORD_ROUTE",
@@ -50,7 +50,12 @@ const mutations: MutationTree<tokenStateInterface> = {
50
50
  const actions: ActionTree<tokenStateInterface, any> = {
51
51
  updateRaw({ commit }, value: string) {
52
52
  commit("setRaw", value);
53
- const parseJwt = JSON.parse(window.atob(value.split(".")[1]));
53
+ let parseJwt: string
54
+ try {
55
+ parseJwt = JSON.parse(window.atob(value.split(".")[1]));
56
+ } catch (error) {
57
+ parseJwt = ""
58
+ }
54
59
  commit("setPayload", parseJwt)
55
60
  }
56
61
  }