@sowell/quasar-app-extension-auth 0.3.4 → 0.3.7

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.
@@ -40,7 +40,7 @@
40
40
  class="full-width q-mb-md"
41
41
  >
42
42
  <template #prepend>
43
- <q-icon :name="ionLockClosedOutline" />
43
+ <q-icon :name="icons.ionLockClosedOutline" />
44
44
  </template>
45
45
  </q-input>
46
46
  </div>
@@ -60,7 +60,10 @@
60
60
  </q-form>
61
61
  </div>
62
62
  <q-dialog v-model="passwordUpdated" persistent>
63
- <sowell-message :passwordUpdated="passwordUpdated" @goToLoginPage="goToLoginPage"/>
63
+ <sowell-message
64
+ :passwordUpdated="passwordUpdated"
65
+ @goToLoginPage="goToLoginPage"
66
+ />
64
67
  </q-dialog>
65
68
  </q-page>
66
69
  </q-layout>
@@ -68,210 +71,36 @@
68
71
 
69
72
  <script lang="ts">
70
73
  //@ts-ignore
71
- import { computed, defineComponent, ref, Ref, onMounted } from "vue";
74
+ import { defineComponent } from "vue";
72
75
  import SowellMessage from "../dialogs/Message.vue";
73
- import { useQuasar } from "quasar";
74
- import { useI18n } from "vue-i18n";
75
- import { $api, $prompts, $router } from "../../injects";
76
- import jwt from 'jwt-decode'
77
- import { Buffer } from 'buffer/' // NOTE: the trailing '/' is intentional and required by buffer
78
- import {
79
- ionLockClosedOutline,
80
- ionCloseOutline,
81
- ionAlertCircleOutline
82
- } from "@quasar/extras/ionicons-v5";
83
-
84
- interface DecodedToken { // FIXME: consider to move to its own file
85
- sub: number,
86
- email: string,
87
- token: string
88
- }
76
+ import { useResetPassword } from "composables/resetPassword";
89
77
 
90
78
  export default defineComponent({
91
79
  name: "ResetPassword",
92
80
  components: { SowellMessage },
93
81
  setup() {
94
- const { patch, post } = $api();
95
82
  const {
96
- AUTH_SERVER_SIGNING_ROUTE,
97
- AUTH_SERVER_UPDATE_PASSWORD_ROUTE,
83
+ password,
84
+ loading,
85
+ passwordUpdated,
86
+ validations,
98
87
  LOCAL_LOGIN_ROUTE,
99
- } = $prompts();
100
- const $q = useQuasar();
101
- const router = $router()
102
- const $route = router.currentRoute.value
103
- const password = ref("");
104
- const loading = ref(false);
105
- const passwordUpdated = ref(false)
106
- const { t } = useI18n();
107
-
108
- const token = ref("")
109
-
110
- const decodedToken = computed(() => {
111
- if (!token.value) return null
112
- return jwt(token.value) as DecodedToken
113
- }) as Ref<DecodedToken | null>
114
-
115
- const canSubmit = computed(() => {
116
- return decodedToken.value !== null && password.value.length >= 6
117
- });
118
-
119
- const validations = {
120
- passwordRules: [
121
- (val: string) => !!val || t("auth.passwordPresenceError"),
122
- (val: string) => val.length >= 6 || t("auth.passwordLengthError")
123
- ],
124
- passwordConfirmationRules: [
125
- (val: string) =>
126
- val === password.value || t("auth.newPasswordConfirmationFailed"),
127
- (val: string) => !!val || t("auth.passwordPresenceError"),
128
- (val: string) => val.length >= 6 || t("auth.passwordLengthError"),
129
- ],
130
- };
131
-
132
- const decodeKey = (key: string) => {
133
- try {
134
- // remove backslash to avoid decoding error
135
- key = key.replace(/\\n/g, '')
136
- const decodedKey = Buffer.from(key, 'base64').toString('utf-8').split(',')
137
- // split instead of JSON.parse as JSON.parse doesn't work
138
-
139
- if (decodedKey.length !== 2) {
140
- throw 'Invalid encoded key'
141
- }
142
-
143
- const email = decodedKey[0].replace('{', '').split(':')[1]
144
- const tmpToken = decodedKey[1].replace('}', '').split(':')[1]
145
- return {
146
- decoded: true,
147
- email,
148
- tmpToken
149
- }
150
- } catch (err) {
151
- console.log('decodeKey err', err)
152
- return {
153
- decoded: false
154
- }
155
- }
156
- }
157
-
158
- const onSubmit = () => {
159
- const updatePasswordPayload = {
160
- data: {
161
- type: "users",
162
- id: decodedToken.value.sub,
163
- attributes:
164
- {
165
- password: password.value
166
- }
167
- }
168
- }
169
- const updatePasswordHeaders = {
170
- headers: {
171
- 'Authorization': `Bearer ${token.value}`
172
- }
173
- }
174
-
175
- patch(
176
- AUTH_SERVER_UPDATE_PASSWORD_ROUTE.replace(':userId', String(decodedToken.value.sub)),
177
- updatePasswordPayload,
178
- updatePasswordHeaders
179
- ).then(() => {
180
- passwordUpdated.value = true;
181
- }).catch(() => {
182
- $q.notify({
183
- message: t("auth.passwordRecoveryFailed"),
184
- color: "negative",
185
- icon:ionAlertCircleOutline,
186
- position: "top",
187
- actions: [
188
- {
189
- color: "white",
190
- icon: ionCloseOutline,
191
- },
192
- ],
193
- });
194
- });
195
- };
196
-
197
- const goToLoginPage = () => {
198
- router.push(LOCAL_LOGIN_ROUTE);
199
- }
200
-
201
- onMounted(() => {
202
- let key = $route.query.key as string
203
- const { decoded, email, tmpToken } = decodeKey(key)
204
-
205
- if (!decoded) {
206
- return $q.notify({
207
- message: t("auth.passwordRecoveryInvalidLink"),
208
- color: "negative",
209
- position: "top",
210
- icon: ionAlertCircleOutline,
211
- actions: [
212
- {
213
- color: "white",
214
- icon: ionCloseOutline,
215
- },
216
- ],
217
- })
218
- }
219
-
220
- const tmpAuthPayload = {
221
- auth: {
222
- email: email,
223
- token: tmpToken
224
- }
225
- }
226
-
227
- post(AUTH_SERVER_SIGNING_ROUTE, tmpAuthPayload)
228
- .then((response) => {
229
- token.value = response.data.jwt
230
- if (!token.value) throw 'Error reading the temporary token'
231
- })
232
- .catch((error) => {
233
- if (error.response) {
234
- // Request made and server responded
235
- if (error.response.status === 404) {
236
- return $q.notify({
237
- message: t("auth.passwordRecoveryExpiredLink"),
238
- color: "negative",
239
- icon: ionAlertCircleOutline,
240
- position: "top",
241
- actions: [
242
- {
243
- color: "white",
244
- icon: ionCloseOutline,
245
- },
246
- ],
247
- });
248
- }
249
- }
250
- return $q.notify({
251
- message: t("auth.passwordRecoveryFailed"),
252
- color: "negative",
253
- position: "top",
254
- icon: ionAlertCircleOutline,
255
- actions: [
256
- {
257
- color: "white",
258
- icon: ionCloseOutline,
259
- },
260
- ],
261
- });
262
- });
263
- })
88
+ canSubmit,
89
+ onSubmit,
90
+ goToLoginPage,
91
+ icons,
92
+ } = useResetPassword();
264
93
 
265
94
  return {
266
95
  password,
267
- validations,
268
96
  loading,
269
- onSubmit,
270
97
  canSubmit,
271
98
  passwordUpdated,
272
99
  LOCAL_LOGIN_ROUTE,
273
- ionLockClosedOutline,
274
- goToLoginPage
100
+ validations,
101
+ onSubmit,
102
+ goToLoginPage,
103
+ icons
275
104
  };
276
105
  },
277
106
  });
@@ -1,67 +1,89 @@
1
1
  <template>
2
2
  <q-layout>
3
3
  <div class="row window-height">
4
- <div class="column col content-center justify-center">
5
- <div style="max-width:500px">
6
- <h4 class="title">{{ $t('auth.title') }}</h4>
7
- <div class="subtitle">{{ $t('auth.subtitle') }}</div>
8
- <q-form @submit="onSubmit">
9
- <div>
10
- <div
11
- class="q-ml-xs q-mb-xs text-subtitle2 text-weight-bold"
12
- >
13
- {{ $t('auth.email') }}
4
+ <div class="col column content-center bgWhite">
5
+ <div class="q-mt-xl">
6
+ <img
7
+ src="../../assets/Sowell-logo.svg"
8
+ style="width: 210px"
9
+ class="logoWhite"
10
+ />
11
+ </div>
12
+ <div class="column col content-center justify-center bgWhite">
13
+ <div style="max-width: 500px">
14
+ <h4 class="title">{{ WELCOME_TITLE }}</h4>
15
+ <div class="subtitle">{{ WELCOME_SUBTITLE }}</div>
16
+ <q-form @submit="onSubmit">
17
+ <div>
18
+ <div class="q-ml-xs q-mb-xs text-subtitle2 text-weight-bold">
19
+ {{ $t("auth.email") }}
20
+ </div>
21
+ <q-input
22
+ v-model.trim="email"
23
+ type="email"
24
+ :rules="validations['email']"
25
+ lazy-rules
26
+ dense
27
+ outlined
28
+ input-class="text-subtitle2 text-weight-medium"
29
+ />
30
+ <div class="q-ml-xs q-mb-xs text-subtitle2 text-weight-bold">
31
+ {{ $t("auth.password") }}
32
+ </div>
33
+ <q-input
34
+ v-model="password"
35
+ :type="showPassword ? 'text' : 'password'"
36
+ :rules="validations['password']"
37
+ lazy-rules
38
+ dense
39
+ outlined
40
+ input-class="text-subtitle2 text-weight-bold"
41
+ >
42
+ <template #append>
43
+ <q-icon
44
+ :name="
45
+ showPassword ? icons.ionEyeSharp : icons.ionEyeOffSharp
46
+ "
47
+ class="cursor-pointer q-mr-sm lightenDark"
48
+ @click="showPassword = !showPassword"
49
+ />
50
+ </template>
51
+ </q-input>
14
52
  </div>
15
- <q-input
16
- v-model.trim="email"
17
- type="email"
18
- outlined
19
- :rules="validations['email']"
20
- lazy-rules
21
- dense
22
- input-class="text-subtitle2 text-weight-medium"
23
- />
24
- <div
25
- class="q-ml-xs q-mb-xs text-subtitle2 text-weight-bold"
26
- >
27
- {{ $t('auth.password') }}
53
+ <div class="row justify-between items-center">
54
+ <q-btn
55
+ :label="$t('auth.login')"
56
+ color="primary"
57
+ no-caps
58
+ unelevated
59
+ text-color="white"
60
+ :loading="loading"
61
+ type="submit"
62
+ :disable="isSubmitBtnDisabled()"
63
+ class="submitBtn text-subtitle2 text-weight-bold"
64
+ />
65
+ <div
66
+ class="cursor-pointer text-primary text-subtitle2"
67
+ @click="forgottenPassword = true"
68
+ >
69
+ {{ $t("auth.forgottenPassword") }}
70
+ </div>
28
71
  </div>
29
- <q-input
30
- v-model="password"
31
- :type="showPassword ? 'text' : 'password'"
32
- :rules="validations['password']"
33
- lazy-rules
34
- outlined
35
- dense
36
- input-class="text-subtitle2 text-weight-bold"
37
- >
38
- <template #append>
39
- <q-icon
40
- :name="showPassword ? ionEyeOutline : ionEyeOffOutline"
41
- class="cursor-pointer q-mr-sm"
42
- @click="showPassword = !showPassword"
43
- />
44
- </template>
45
- </q-input>
46
- </div>
47
- <div class="row justify-between items-center">
48
- <q-btn
49
- :label="$t('auth.login')"
50
- color="primary"
51
- no-caps
52
- unelevated
53
- text-color="white"
54
- :loading="loading"
55
- type="submit"
56
- :disable="submitBtnDisabled"
57
- />
58
- <div class="cursor-pointer" @click="forgottenPassword = true">{{ $t('auth.forgottenPassword') }}</div>
59
- </div>
60
- </q-form>
72
+ </q-form>
73
+ </div>
61
74
  </div>
62
75
  </div>
63
- <div class="column col bgBlue justify-center content-center">
64
- <img src="../../assets/SowellWhite.svg" class="logoWhite"/>
76
+ <div
77
+ class="column col justify-center content-center"
78
+ :style="isManager ? 'background: #eef8fd' : ''"
79
+ :class="!isManager ? 'bgBlue' : ''"
80
+ >
81
+ <img
82
+ v-if="isManager"
83
+ src="../../assets/illustration-login.svg"
84
+ style="width: 600px"
85
+ />
86
+ <img v-else src="../../assets/SowellWhite.svg" class="logoWhite" />
65
87
  </div>
66
88
  </div>
67
89
  <div>
@@ -73,14 +95,13 @@
73
95
  transition-hide="fade"
74
96
  persistent
75
97
  >
76
- <send-email @resetPassword="resetPassword" />
98
+ <web-send-email @resetPassword="resetPassword" />
77
99
  </q-dialog>
78
- <q-dialog v-model="isReset" persistent>
79
- <sowell-message
80
- :emailResetPassword="emailResetPassword"
81
- :sendEmailSuccess="sendEmailSuccess"
82
- :loading="loading"
83
- @retypeEmail="retypeEmail"
100
+ <q-dialog v-model="isNotification" persistent>
101
+ <sw-info-dialog
102
+ :title="infoDialog.title"
103
+ :content="infoDialog.content"
104
+ :image="infoDialog.image"
84
105
  />
85
106
  </q-dialog>
86
107
  </div>
@@ -89,203 +110,132 @@
89
110
 
90
111
  <script lang="ts">
91
112
  import isEmail from "validator/es/lib/isEmail";
92
- import { defineComponent, ref, onMounted, computed } from "vue";
93
- import { $api, $prompts, $router, $store } from "../../injects";
94
- import { useQuasar } from "quasar";
113
+ import { defineComponent, ref, computed } from "vue";
114
+ import { useAuthState } from "../../injects";
95
115
  import { useI18n } from "vue-i18n";
96
- import { AxiosError, AxiosResponse } from "axios";
97
- import prompts from "app/quasar.extensions.json";
98
- import SendEmail from "../dialogs/SendEmail.vue";
99
- import SowellMessage from "../dialogs/Message.vue";
100
- import {
101
- ionMailOutline,
102
- ionLockClosedOutline,
103
- ionEyeOutline,
104
- ionEyeOffOutline,
105
- ionAlertCircleOutline,
106
- ionCloseOutline
107
- } from "@quasar/extras/ionicons-v5";
116
+ import SwInfoDialog from "../dialogs/sw-info-dialog.vue";
117
+ import WebSendEmail from "../dialogs/WebSendEmail.vue";
118
+
119
+ import { useAuth } from "composables/auth";
108
120
 
109
121
  interface HeadersInterface {
110
122
  headers: {
111
- "x-requested-roles"?: string
112
- }
123
+ "x-requested-roles"?: string;
124
+ };
113
125
  }
114
126
  export default defineComponent({
115
- name: 'Auth',
116
- components: { SendEmail, SowellMessage },
127
+ name: "Auth",
128
+ components: { WebSendEmail, SwInfoDialog },
117
129
  setup() {
118
- const {
119
- "@sowell/auth": { X_REQUESTED_ROLES },
120
- } = prompts;
121
- const email = ref("");
122
- const password = ref("");
123
- const loading = ref(false);
124
- const showPassword = ref(false);
125
- const forgottenPassword = ref(false);
126
- const isReset = ref(false);
127
- const sendEmailSuccess = ref(false);
128
- const emailResetPassword = ref("");
129
- const { post } = $api();
130
- const {
131
- LOCAL_SUCCESS_REDIRECTION_ROUTE,
132
- AUTH_SERVER_SIGNING_ROUTE,
133
- AUTH_SERVER_RESET_PASSWORD_ROUTE,
134
- APP_SERVER_BASE_URL,
135
- } = $prompts();
136
- const $q = useQuasar();
137
- const { push, replace } = $router();
138
- const { dispatch, getters } = $store();
139
130
  const { t } = useI18n();
131
+ const authState = useAuthState();
140
132
 
141
- onMounted(() => {
142
- if (getters["token/getRaw"]) {
143
- replace(LOCAL_SUCCESS_REDIRECTION_ROUTE);
144
- }
145
- });
133
+ const {
134
+ email,
135
+ password,
136
+ loading,
137
+ showPassword,
138
+ validations,
139
+ forgottenPassword,
140
+ emailResetPassword,
141
+ WELCOME_TITLE,
142
+ WELCOME_SUBTITLE,
143
+ X_REQUESTED_ROLES,
144
+ APP_SERVER_BASE_URL,
145
+ onSubmit,
146
+ retypeEmail,
147
+ isSubmitBtnDisabled,
148
+ icons,
149
+ } = useAuth();
146
150
 
147
- const validations = ref({
148
- email: [
149
- (val: string) => !!val || t("auth.emailPresenceError"),
150
- (val: string) => isEmail(val.trim()) || t("auth.emailValidationError"),
151
- ],
152
- password: [(val: string) => !!val || t("auth.passwordPresenceError")],
151
+ const isNotification = ref(false);
152
+ const infoDialog = ref({
153
+ title: "",
154
+ content: "",
155
+ image: "",
153
156
  });
154
157
 
155
- const resetPassword = (email: string) => {
158
+ const submitBtnDisabled = computed(
159
+ () =>
160
+ !(
161
+ email.value &&
162
+ isEmail(email.value) &&
163
+ password.value &&
164
+ password.value.length > 5
165
+ ),
166
+ );
167
+ const isManager = computed(() => X_REQUESTED_ROLES === "clean_manager");
168
+ const resetPassword = async (email: string) => {
156
169
  const data = {
157
170
  auth: {
158
171
  email,
159
172
  base: APP_SERVER_BASE_URL,
160
173
  },
161
174
  };
162
-
163
-
164
- post(AUTH_SERVER_RESET_PASSWORD_ROUTE, data)
165
- .then(() => {
166
- forgottenPassword.value = false;
167
- isReset.value = true;
168
- sendEmailSuccess.value = true;
169
- })
170
- .catch(() => {
171
- isReset.value = true;
172
- });
173
- };
174
-
175
- const retypeEmail = () => {
176
- forgottenPassword.value = true
177
- }
178
-
179
- const onSubmit = () => {
180
- loading.value = true;
181
- const data = {
182
- auth: {
183
- email: email.value,
184
- password: password.value,
185
- },
186
- };
187
- const options: HeadersInterface = {
188
- headers:{}
189
- };
190
- // @ts-ignore
191
- let requestedRoles = window.Cypress ? window.Cypress.requestedRoles : X_REQUESTED_ROLES;
192
- if (requestedRoles) options.headers["x-requested-roles"] = requestedRoles;
193
-
194
- post(AUTH_SERVER_SIGNING_ROUTE, data, options)
195
- .then((response: AxiosResponse) => {
196
- const token = response.data.jwt;
197
- dispatch("token/updateRaw", token);
198
- push(LOCAL_SUCCESS_REDIRECTION_ROUTE);
199
- })
200
- .catch((error: AxiosError) => {
201
- loading.value = false;
202
- switch (error.response?.status) {
203
- case 403:
204
- $q.notify({
205
- message: t("auth.403"),
206
- color: "negative",
207
- icon: ionAlertCircleOutline,
208
- position: "top",
209
- actions: [
210
- {
211
- color: "white",
212
- icon: ionCloseOutline,
213
- },
214
- ],
215
- });
216
- break;
217
- case 404:
218
- $q.notify({
219
- message: t("auth.404"),
220
- color: "negative",
221
- icon: ionAlertCircleOutline,
222
- position: "top",
223
- actions: [
224
- {
225
- color: "white",
226
- icon: ionCloseOutline,
227
- },
228
- ],
229
- });
230
- break;
231
- default:
232
- $q.notify({
233
- message: t("auth.loginFailed"),
234
- color: "negative",
235
- icon: ionAlertCircleOutline,
236
- position: "top",
237
- actions: [
238
- {
239
- color: "white",
240
- icon: ionCloseOutline,
241
- },
242
- ],
243
- });
244
- }
245
- });
175
+ try {
176
+ await authState.resetPassword(data);
177
+ forgottenPassword.value = false;
178
+ infoDialog.value = {
179
+ title: t("auth.mailVerificationTitle"),
180
+ content: t("auth.mailVerificationSubtitle"),
181
+ image: "emailVerification",
182
+ };
183
+ forgottenPassword.value = false;
184
+ isNotification.value = true;
185
+ } catch {
186
+ infoDialog.value = {
187
+ title: t("auth.userNotFoundTitle"),
188
+ content: t("auth.userNotFoundSubtitle"),
189
+ image: "userNotFound",
190
+ };
191
+ isNotification.value = true;
192
+ }
246
193
  };
247
194
 
248
- const submitBtnDisabled = computed(() => !(
249
- email.value &&
250
- isEmail(email.value) &&
251
- password.value &&
252
- password.value.length > 5
253
- )
254
- )
255
195
  return {
256
196
  email,
197
+ infoDialog,
257
198
  password,
258
199
  loading,
259
200
  showPassword,
260
201
  validations,
261
202
  submitBtnDisabled,
262
- onSubmit,
263
203
  forgottenPassword,
264
204
  emailResetPassword,
205
+ isNotification,
206
+ isManager,
207
+ WELCOME_SUBTITLE,
208
+ WELCOME_TITLE,
209
+ isSubmitBtnDisabled,
265
210
  resetPassword,
266
- isReset,
267
211
  retypeEmail,
268
- sendEmailSuccess,
269
- ionMailOutline,
270
- ionLockClosedOutline,
271
- ionEyeOutline,
272
- ionEyeOffOutline,
273
- }
212
+ onSubmit,
213
+ icons,
214
+ };
274
215
  },
275
- })
216
+ });
276
217
  </script>
277
218
 
278
219
  <style scoped>
220
+ .bgWhite {
221
+ background: #ffffff;
222
+ }
279
223
  .bgBlue {
280
224
  background: var(--q-primary);
281
225
  }
226
+ .lightenDark {
227
+ color: rgba(0, 0, 0, 0.24);
228
+ }
282
229
  .title {
283
230
  margin: 0px 0px 16px 0px;
284
231
  }
285
232
  .subtitle {
286
- margin-bottom: 104px
233
+ margin-bottom: 80px;
287
234
  }
288
235
  .logoWhite {
289
- width: 30%
236
+ width: 30%;
237
+ }
238
+ .submitBtn {
239
+ width: 254px;
290
240
  }
291
241
  </style>