@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.
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <q-layout>
3
+ <q-page class="q-pa-md row justify-center">
4
+ <div>
5
+ <div class="q-mt-xl">
6
+ <img
7
+ src="../../assets/Sowell-logo.svg"
8
+ class="q-mb-sm q-mr-md"
9
+ style="width: 190px"
10
+ />
11
+ </div>
12
+ <div
13
+ class="column justify-center full-height"
14
+ style="max-width: 550px; margin-top: -90px"
15
+ >
16
+ <div></div>
17
+ <div
18
+ class="text-subtitle1 q-mt-lg"
19
+ style="font-size: 34px; font-weight: 600; line-height: 34px"
20
+ >
21
+ {{ $t("auth.passwordRecovery") }}
22
+ </div>
23
+ <div
24
+ class="text-subtitle1 q-mt-lg q-mb-lg"
25
+ style="font-size: 16px; font-weight: 400; line-height: 24px"
26
+ >
27
+ {{ $t("auth.recommendPassword") }}
28
+ </div>
29
+ <q-form class="column form" @submit="onSubmit">
30
+ <div>
31
+ <div class="q-mb-sm" style="font-size: 14px; font-weight: 600">
32
+ {{ $t("auth.newPassword") }}
33
+ </div>
34
+ <q-input
35
+ id="password"
36
+ v-model="password"
37
+ data-cy="reset-password"
38
+ :rules="validations['passwordRules']"
39
+ outlined
40
+ :type="showPassword ? 'text' : 'password'"
41
+ bg-color="white"
42
+ dense
43
+ class="full-width q-mb-md"
44
+ >
45
+ <template #append>
46
+ <q-icon
47
+ :name="
48
+ showPassword
49
+ ? icons.ionEyeOffOutline
50
+ : icons.ionEyeOutline
51
+ "
52
+ class="cursor-pointer q-mr-sm"
53
+ @click="showPassword = !showPassword"
54
+ />
55
+ </template>
56
+ </q-input>
57
+ </div>
58
+ <q-space class="mobile" />
59
+ <div>
60
+ <q-btn
61
+ :label="$t('common.register')"
62
+ color="primary"
63
+ :loading="loading"
64
+ type="submit"
65
+ no-caps
66
+ class="submitBtn"
67
+ data-cy="send-new-password"
68
+ :disable="!canSubmit"
69
+ />
70
+ </div>
71
+ </q-form>
72
+ </div>
73
+ </div>
74
+ <q-dialog v-model="passwordUpdated" persistent>
75
+ <sw-info-dialog
76
+ :title="$t('auth.passwordChanged')"
77
+ :content="$t('auth.passwordUpdated')"
78
+ :image="'passwordUpdated'"
79
+ :passwordUpdated="passwordUpdated"
80
+ @goToLoginPage="goToLoginPage"
81
+ />
82
+ </q-dialog>
83
+ </q-page>
84
+ </q-layout>
85
+ </template>
86
+
87
+ <script lang="ts">
88
+ //@ts-ignore
89
+ import { defineComponent } from "vue";
90
+ import SwInfoDialog from "../dialogs/sw-info-dialog.vue";
91
+ import { useResetPassword } from "composables/resetPassword";
92
+
93
+ export default defineComponent({
94
+ name: "ResetPassword",
95
+ components: { SwInfoDialog },
96
+ setup() {
97
+ const {
98
+ password,
99
+ loading,
100
+ passwordUpdated,
101
+ showPassword,
102
+ validations,
103
+ canSubmit,
104
+ onSubmit,
105
+ goToLoginPage,
106
+ icons
107
+ } = useResetPassword();
108
+
109
+ return {
110
+ password,
111
+ validations,
112
+ loading,
113
+ canSubmit,
114
+ passwordUpdated,
115
+ showPassword,
116
+ goToLoginPage,
117
+ onSubmit,
118
+ icons
119
+ };
120
+ },
121
+ });
122
+ </script>
123
+
124
+ <style scoped>
125
+ .submitBtn {
126
+ width: 254px;
127
+ }
128
+ </style>
@@ -0,0 +1,120 @@
1
+ import { ref, onMounted } from "vue";
2
+ import isEmail from "validator/es/lib/isEmail";
3
+ import { useI18n } from "vue-i18n";
4
+ import {
5
+ ionMailOutline,
6
+ ionLockClosedOutline,
7
+ ionEyeOutline,
8
+ ionEyeOffOutline,
9
+ ionEyeSharp,
10
+ ionEyeOffSharp,
11
+ } from "@quasar/extras/ionicons-v5";
12
+
13
+ import { $prompts, $router, useAuthState } from "../injects";
14
+
15
+ interface HeadersInterface {
16
+ headers: {
17
+ "x-requested-roles"?: string;
18
+ };
19
+ }
20
+
21
+ export function useAuth() {
22
+ const authState = useAuthState();
23
+ const { t } = useI18n();
24
+ const {
25
+ LOCAL_SUCCESS_REDIRECTION_ROUTE,
26
+ APP_SERVER_BASE_URL,
27
+ X_REQUESTED_ROLES,
28
+ WELCOME_TITLE,
29
+ WELCOME_SUBTITLE,
30
+ } = $prompts();
31
+ const { push, replace } = $router();
32
+
33
+ const email = ref("");
34
+ const password = ref("");
35
+ const loading = ref(false);
36
+ const showPassword = ref(false);
37
+ const forgottenPassword = ref(false);
38
+ const isReset = ref(false);
39
+ const sendEmailSuccess = ref(false);
40
+ const emailResetPassword = ref("");
41
+ const validations = ref({
42
+ email: [
43
+ (val: string) => !!val || t("auth.emailPresenceError"),
44
+ (val: string) => isEmail(val.trim()) || t("auth.emailValidationError"),
45
+ ],
46
+ password: [(val: string) => !!val || t("auth.passwordPresenceError")],
47
+ });
48
+
49
+ const retypeEmail = () => {
50
+ forgottenPassword.value = true;
51
+ };
52
+
53
+ const onSubmit = async () => {
54
+ loading.value = true;
55
+ const data = {
56
+ auth: {
57
+ email: email.value,
58
+ password: password.value,
59
+ },
60
+ };
61
+ const options: HeadersInterface = {
62
+ headers: {},
63
+ };
64
+ // @ts-ignore
65
+ let requestedRoles = window.Cypress
66
+ ? window.Cypress.requestedRoles
67
+ : X_REQUESTED_ROLES;
68
+ if (requestedRoles) options.headers["x-requested-roles"] = requestedRoles;
69
+
70
+ try {
71
+ await authState.signin(data, options);
72
+ push(LOCAL_SUCCESS_REDIRECTION_ROUTE);
73
+ } catch (err) {
74
+ loading.value = false;
75
+ }
76
+ };
77
+
78
+ const isSubmitBtnDisabled = () => {
79
+ return !(
80
+ email.value &&
81
+ isEmail(email.value) &&
82
+ password.value &&
83
+ password.value.length > 5
84
+ );
85
+ };
86
+
87
+ onMounted(() => {
88
+ if (authState.raw) {
89
+ replace(LOCAL_SUCCESS_REDIRECTION_ROUTE);
90
+ }
91
+ });
92
+
93
+ return {
94
+ email,
95
+ password,
96
+ loading,
97
+ showPassword,
98
+ validations,
99
+ forgottenPassword,
100
+ emailResetPassword,
101
+ isReset,
102
+ sendEmailSuccess,
103
+ authState,
104
+ APP_SERVER_BASE_URL,
105
+ WELCOME_TITLE,
106
+ WELCOME_SUBTITLE,
107
+ X_REQUESTED_ROLES,
108
+ onSubmit,
109
+ retypeEmail,
110
+ isSubmitBtnDisabled,
111
+ icons: {
112
+ ionMailOutline,
113
+ ionLockClosedOutline,
114
+ ionEyeOutline,
115
+ ionEyeOffOutline,
116
+ ionEyeSharp,
117
+ ionEyeOffSharp,
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,219 @@
1
+ import { useQuasar } from "quasar";
2
+ import { useI18n } from "vue-i18n";
3
+ import jwt from "jwt-decode";
4
+ import { computed, ref, Ref, onMounted } from "vue";
5
+ import {
6
+ ionCloseOutline,
7
+ ionEyeOutline,
8
+ ionEyeOffOutline,
9
+ ionAlertCircleOutline,
10
+ ionLockClosedOutline,
11
+ } from "@quasar/extras/ionicons-v5";
12
+
13
+ import { $api, $prompts, $router } from "../injects";
14
+ import { Buffer } from "buffer/"; // NOTE: the trailing '/' is intentional and required by buffer
15
+
16
+ interface DecodedToken {
17
+ sub: number;
18
+ email: string;
19
+ token: string;
20
+ }
21
+ export function useResetPassword() {
22
+ const $q = useQuasar();
23
+ const { t } = useI18n();
24
+
25
+ const password = ref("");
26
+ const loading = ref(false);
27
+ const passwordUpdated = ref(false);
28
+ const showPassword = ref(false);
29
+ const token = ref("");
30
+
31
+ const { patch, post } = $api();
32
+ const router = $router();
33
+ const {
34
+ AUTH_SERVER_SIGNING_ROUTE,
35
+ AUTH_SERVER_UPDATE_PASSWORD_ROUTE,
36
+ LOCAL_LOGIN_ROUTE,
37
+ } = $prompts();
38
+ const $route = router.currentRoute.value;
39
+
40
+ const decodedToken = computed(() => {
41
+ if (!token.value) return null;
42
+ return jwt(token.value) as DecodedToken;
43
+ }) as Ref<DecodedToken | null>;
44
+
45
+ const canSubmit = computed(() => {
46
+ return decodedToken.value !== null && password.value.length >= 6;
47
+ });
48
+
49
+ const validations = {
50
+ passwordRules: [
51
+ (val: string) => !!val || t("auth.passwordPresenceError"),
52
+ (val: string) => val.length >= 6 || t("auth.passwordLengthError"),
53
+ ],
54
+ passwordConfirmationRules: [
55
+ (val: string) =>
56
+ val === password.value || t("auth.newPasswordConfirmationFailed"),
57
+ (val: string) => !!val || t("auth.passwordPresenceError"),
58
+ (val: string) => val.length >= 6 || t("auth.passwordLengthError"),
59
+ ],
60
+ };
61
+
62
+ const decodeKey = (key: string) => {
63
+ try {
64
+ // remove backslash to avoid decoding error
65
+ key = key.replace(/\\n/g, "");
66
+ const decodedKey = Buffer.from(key, "base64")
67
+ .toString("utf-8")
68
+ .split(",");
69
+ // split instead of JSON.parse as JSON.parse doesn't work
70
+
71
+ if (decodedKey.length !== 2) {
72
+ throw "Invalid encoded key";
73
+ }
74
+
75
+ const email = decodedKey[0].replace("{", "").split(":")[1];
76
+ const tmpToken = decodedKey[1].replace("}", "").split(":")[1];
77
+ return {
78
+ decoded: true,
79
+ email,
80
+ tmpToken,
81
+ };
82
+ } catch (err) {
83
+ return {
84
+ decoded: false,
85
+ };
86
+ }
87
+ };
88
+
89
+ const onSubmit = () => {
90
+ const updatePasswordPayload = {
91
+ data: {
92
+ type: "users",
93
+ id: decodedToken.value.sub,
94
+ attributes: {
95
+ password: password.value,
96
+ },
97
+ },
98
+ };
99
+ const updatePasswordHeaders = {
100
+ headers: {
101
+ Authorization: `Bearer ${token.value}`,
102
+ },
103
+ };
104
+ loading.value = true;
105
+
106
+ patch(
107
+ AUTH_SERVER_UPDATE_PASSWORD_ROUTE.replace(
108
+ ":userId",
109
+ String(decodedToken.value.sub),
110
+ ),
111
+ updatePasswordPayload,
112
+ updatePasswordHeaders,
113
+ )
114
+ .then(() => {
115
+ loading.value = false;
116
+ passwordUpdated.value = true;
117
+ })
118
+ .catch(() => {
119
+ loading.value = false;
120
+ $q.notify({
121
+ message: t("auth.passwordRecoveryFailed"),
122
+ color: "negative",
123
+ icon: ionAlertCircleOutline,
124
+ position: "top",
125
+ actions: [
126
+ {
127
+ color: "white",
128
+ icon: ionCloseOutline,
129
+ },
130
+ ],
131
+ });
132
+ });
133
+ };
134
+
135
+ const goToLoginPage = () => {
136
+ router.push(LOCAL_LOGIN_ROUTE);
137
+ };
138
+
139
+ onMounted(() => {
140
+ let key = $route.query.key as string;
141
+ const { decoded, email, tmpToken } = decodeKey(key);
142
+
143
+ if (!decoded) {
144
+ return $q.notify({
145
+ message: t("auth.passwordRecoveryInvalidLink"),
146
+ color: "negative",
147
+ position: "top",
148
+ icon: ionAlertCircleOutline,
149
+ actions: [
150
+ {
151
+ color: "white",
152
+ icon: ionCloseOutline,
153
+ },
154
+ ],
155
+ });
156
+ }
157
+
158
+ const tmpAuthPayload = {
159
+ auth: {
160
+ email: email,
161
+ token: tmpToken,
162
+ },
163
+ };
164
+
165
+ post(AUTH_SERVER_SIGNING_ROUTE, tmpAuthPayload)
166
+ .then((response) => {
167
+ token.value = response.data.jwt;
168
+ if (!token.value) throw "Error reading the temporary token";
169
+ })
170
+ .catch((error) => {
171
+ if (error.response) {
172
+ // Request made and server responded
173
+ if (error.response.status === 404) {
174
+ return $q.notify({
175
+ message: t("auth.passwordRecoveryExpiredLink"),
176
+ color: "negative",
177
+ icon: ionAlertCircleOutline,
178
+ position: "top",
179
+ actions: [
180
+ {
181
+ color: "white",
182
+ icon: ionCloseOutline,
183
+ },
184
+ ],
185
+ });
186
+ }
187
+ }
188
+ return $q.notify({
189
+ message: t("auth.passwordRecoveryFailed"),
190
+ color: "negative",
191
+ position: "top",
192
+ icon: ionAlertCircleOutline,
193
+ actions: [
194
+ {
195
+ color: "white",
196
+ icon: ionCloseOutline,
197
+ },
198
+ ],
199
+ });
200
+ });
201
+ });
202
+
203
+ return {
204
+ password,
205
+ loading,
206
+ passwordUpdated,
207
+ showPassword,
208
+ validations,
209
+ canSubmit,
210
+ LOCAL_LOGIN_ROUTE,
211
+ onSubmit,
212
+ goToLoginPage,
213
+ icons: {
214
+ ionEyeOutline,
215
+ ionEyeOffOutline,
216
+ ionLockClosedOutline,
217
+ },
218
+ };
219
+ }
package/src/index.js CHANGED
@@ -29,6 +29,10 @@ const chainWebpack = (ctx, chain) => {
29
29
  "@sowell/auth",
30
30
  path.resolve(__dirname, "./components")
31
31
  );
32
+ chain.resolve.alias.set(
33
+ "composables",
34
+ path.resolve(__dirname, "./composables")
35
+ );
32
36
  };
33
37
  module.exports = function (api) {
34
38
  api.compatibleWith("quasar", "^2.0.0");
@@ -2,3 +2,4 @@ export * from "./api";
2
2
  export * from "./prompts";
3
3
  export * from "./router";
4
4
  export * from "./store";
5
+ export * from './states'
@@ -12,10 +12,12 @@ export interface PromptsInterface {
12
12
  AUTH_TYPE: string;
13
13
  VUEX_SET_TOKEN_MUTATION: string;
14
14
  VUEX_CLEAR_TOKEN_MUTATION: string;
15
- VUEX_SET_EMAIL_OR_PHONENUMBER_USER_MUTATION: string;
16
15
  VUEX_SET_INFOS_USER_MUTATION: string;
17
16
  VUEX_GET_TOKEN_GETTER: string;
18
17
  VUEX_GET_INFOS_USER_GETTER: string;
18
+ X_REQUESTED_ROLES: string;
19
+ WELCOME_TITLE: string;
20
+ WELCOME_SUBTITLE: string;
19
21
  }
20
22
  export const $prompts = (): PromptsInterface => {
21
23
  const $prompts = inject<Record<"@sowell/auth", PromptsInterface>>("$prompts");
@@ -0,0 +1,58 @@
1
+ import { inject } from "vue";
2
+ import { Store } from "pinia";
3
+ import { AxiosResponse } from "axios";
4
+
5
+ export interface AuthStatePayload {
6
+ company_hp: string;
7
+ exp: number;
8
+ gm_key: string;
9
+ roles: string[];
10
+ sub: number;
11
+ }
12
+
13
+ export interface AuthState {
14
+ raw?: string;
15
+ payload?: AuthStatePayload;
16
+ }
17
+
18
+ export interface RequestOptions {
19
+ headers: {
20
+ "x-requested-roles"?: string;
21
+ };
22
+ }
23
+
24
+ export interface SignInPayload {
25
+ auth: {
26
+ email: string;
27
+ password: string;
28
+ };
29
+ }
30
+
31
+ export interface ResetPasswordPayload {
32
+ auth: {
33
+ email: string;
34
+ base: string;
35
+ };
36
+ }
37
+
38
+ export const useAuthState = (): Store<
39
+ "auth",
40
+ AuthState,
41
+ {
42
+ getIsValid(state: AuthState): boolean;
43
+ },
44
+ {
45
+ signin(
46
+ data: SignInPayload,
47
+ options: RequestOptions
48
+ ): Promise<AxiosResponse>;
49
+ resetPassword(data: ResetPasswordPayload): Promise<AxiosResponse>;
50
+ updateRaw(value: string): void;
51
+ }
52
+ > => {
53
+ const authState = inject<Store<"auth", AuthState>>("authState");
54
+ if (authState == undefined) {
55
+ throw Error("Could not inject authState");
56
+ }
57
+ return authState;
58
+ };
package/src/prompts.js CHANGED
@@ -113,5 +113,33 @@ module.exports = function () {
113
113
  message: "Role required for the application",
114
114
  default: "",
115
115
  },
116
+ {
117
+ name: "FORGOTTEN_PASSWORD_TITLE",
118
+ type: "input",
119
+ required: true,
120
+ message: "Title on forgotten password page",
121
+ default: "",
122
+ },
123
+ {
124
+ name: "FORGOTTEN_PASSWORD_SUBTITLE",
125
+ type: "input",
126
+ required: true,
127
+ message: "Subtitle on forgotten password page",
128
+ default: "",
129
+ },
130
+ {
131
+ name: "WELCOME_TITLE",
132
+ type: "input",
133
+ required: true,
134
+ message: "Title on login page",
135
+ default: "",
136
+ },
137
+ {
138
+ name: "WELCOME_SUBTITLE",
139
+ type: "input",
140
+ required: true,
141
+ message: "Subtitle on login page",
142
+ default: "",
143
+ },
116
144
  ];
117
145
  };
@@ -18,7 +18,14 @@ const routes: RouteRecordRaw[] = [
18
18
  path: LOCAL_UPDATE_PASSWORD_ROUTE,
19
19
  name:"update_password",
20
20
  // @ts-ignore
21
- component: () => import("@sowell/auth/pages/ResetPassword.vue"),
21
+ component: () =>
22
+ import(
23
+ `@sowell/auth/pages/${
24
+ webAuthRoles.includes(X_REQUESTED_ROLES)
25
+ ? "WebResetPassword.vue"
26
+ : "ResetPassword.vue"
27
+ }`
28
+ ),
22
29
  },
23
30
  ];
24
31
 
package/tsconfig.json CHANGED
@@ -2,7 +2,6 @@
2
2
  "compilerOptions": {
3
3
  "allowJs": true,
4
4
  "target": "es6",
5
- "module": "commonjs",
6
5
  "strict": true,
7
6
  "moduleResolution": "node",
8
7
  "esModuleInterop": true,