@sowell/quasar-app-extension-auth 0.3.6 → 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.
- package/package.json +4 -2
- package/src/assets/Email-sent.svg +32 -0
- package/src/assets/Illustration-password.svg +79 -0
- package/src/assets/Ok-illustration.svg +13 -0
- package/src/assets/Sowell-logo.svg +4 -0
- package/src/assets/User-not-found.svg +23 -0
- package/src/assets/illustration-login.svg +98 -0
- package/src/boot/router-guards.ts +11 -7
- package/src/components/dialogs/Message.vue +47 -23
- package/src/components/dialogs/SendEmail.vue +8 -5
- package/src/components/dialogs/WebSendEmail.vue +108 -0
- package/src/components/dialogs/sw-info-dialog.vue +87 -0
- package/src/components/pages/Auth.vue +36 -157
- package/src/components/pages/ResetPassword.vue +20 -191
- package/src/components/pages/WebAuth.vue +162 -222
- package/src/components/pages/WebResetPassword.vue +128 -0
- package/src/composables/auth.ts +120 -0
- package/src/composables/resetPassword.ts +219 -0
- package/src/index.js +4 -0
- package/src/injects/index.ts +1 -0
- package/src/injects/prompts.ts +3 -1
- package/src/injects/states.ts +58 -0
- package/src/prompts.js +28 -0
- package/src/routes/index.ts +8 -1
- package/tsconfig.json +0 -1
|
@@ -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");
|
package/src/injects/index.ts
CHANGED
package/src/injects/prompts.ts
CHANGED
|
@@ -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
|
};
|
package/src/routes/index.ts
CHANGED
|
@@ -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: () =>
|
|
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
|
|