@fishawack/lab-velocity 1.2.2 → 1.3.1
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/AuthModule/components/AuthModal.vue +105 -0
- package/AuthModule/js/AuthRoutes.js +4 -1
- package/AuthModule/js/AuthStore.js +6 -0
- package/AuthModule/js/FakeAPI.js +84 -0
- package/AuthModule/routes/change-password.vue +151 -0
- package/AuthModule/routes/force-reset.vue +12 -13
- package/AuthModule/routes/loginsso.vue +6 -1
- package/AuthModule/routes/register.vue +0 -1
- package/modules/_AuthModule.scss +1 -0
- package/modules/_modal.scss +24 -0
- package/package.json +4 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="{'active': open}"
|
|
3
|
+
class="modal modal--auth">
|
|
4
|
+
|
|
5
|
+
<div class="modal__container"
|
|
6
|
+
v-on:click.self="close">
|
|
7
|
+
|
|
8
|
+
<div class="modal__box AuthModule__form" :class="boxCls">
|
|
9
|
+
<component v-if="compName"
|
|
10
|
+
:is="compName"
|
|
11
|
+
v-bind="compProps"
|
|
12
|
+
@close="close"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<button class="button button--modal" @click="open = false"></button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
"use strict";
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
name: "AuthModal",
|
|
26
|
+
|
|
27
|
+
data() {
|
|
28
|
+
return {
|
|
29
|
+
open: false,
|
|
30
|
+
compName: null,
|
|
31
|
+
compProps: null,
|
|
32
|
+
cb: null,
|
|
33
|
+
boxCls: null,
|
|
34
|
+
validComponents: {
|
|
35
|
+
"force-reset" : 'VForceReset',
|
|
36
|
+
"password-change" : 'VPasswordChange'
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
methods: {
|
|
42
|
+
reset(){
|
|
43
|
+
this.compName = null;
|
|
44
|
+
this.compProps = null;
|
|
45
|
+
this.boxCls = null;
|
|
46
|
+
this.cb = null;
|
|
47
|
+
},
|
|
48
|
+
close() {
|
|
49
|
+
if(this.compName === 'VForceReset') {
|
|
50
|
+
if(!this.$store.state.auth.forcePasswordChange) {
|
|
51
|
+
this.open = false;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
this.open = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
created() {
|
|
60
|
+
this.$emitter.on('AuthModal', (comp) => {
|
|
61
|
+
this.compName = this.validComponents[comp.type];
|
|
62
|
+
this.open = comp;
|
|
63
|
+
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
window.addEventListener('keydown', (e) => {
|
|
67
|
+
if (e.key === 'Escape') {
|
|
68
|
+
this.open = false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
watch: {
|
|
74
|
+
open(isOpen) {
|
|
75
|
+
this.$emitter.emit('toggleBackgroundScroll', isOpen);
|
|
76
|
+
|
|
77
|
+
if(this.cb){
|
|
78
|
+
this.cb();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!isOpen) {
|
|
82
|
+
this.reset();
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
'$route'(to,from) {
|
|
86
|
+
if(from.fullPath !== to.fullPath) {
|
|
87
|
+
this.open = false;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
'$store.state.auth.forcePasswordChange': {
|
|
91
|
+
async handler(forcePasswordChange) {
|
|
92
|
+
if(forcePasswordChange) {
|
|
93
|
+
this.compName = this.validComponents["force-reset"];
|
|
94
|
+
this.open = comp;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
components: {
|
|
101
|
+
VForceReset: require("../routes/force-reset.vue").default,
|
|
102
|
+
VPasswordChange: require("../routes/change-password.vue").default,
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
</script>
|
|
@@ -123,13 +123,16 @@ export function authRoutes(node, store, nested = 'auth') {
|
|
|
123
123
|
export function configureRoutes(router) {
|
|
124
124
|
|
|
125
125
|
router.beforeEach((to, from, next) => {
|
|
126
|
-
const { authenticated, user, authBase, redirect } = store.state.auth;
|
|
126
|
+
const { authenticated, user, authBase, redirect, forcePasswordChange } = store.state.auth;
|
|
127
127
|
|
|
128
128
|
const admin = to.path.includes("/admin");
|
|
129
129
|
|
|
130
130
|
if (to.query.verified) {
|
|
131
131
|
next({ name: `${authBase}.success-verify` });
|
|
132
132
|
} else if (authenticated) {
|
|
133
|
+
if(forcePasswordChange) {
|
|
134
|
+
next(false);
|
|
135
|
+
}
|
|
133
136
|
if (admin && !user?.admin) {
|
|
134
137
|
next({ name: redirect });
|
|
135
138
|
} else if (to.name === "login" || to.name === `${authBase}.login`) {
|
|
@@ -6,6 +6,7 @@ const store = {
|
|
|
6
6
|
return {
|
|
7
7
|
authBase : process.env.HYDRATE_ROUTE ?? 'auth',
|
|
8
8
|
authenticated : false,
|
|
9
|
+
forcePasswordChange: false,
|
|
9
10
|
intended: null,
|
|
10
11
|
user: null,
|
|
11
12
|
redirect: process.env.HYDRATE_REDIRECT ?? 'index'
|
|
@@ -30,6 +31,9 @@ const store = {
|
|
|
30
31
|
setIntended(state, value) {
|
|
31
32
|
state.intended = value;
|
|
32
33
|
},
|
|
34
|
+
setForcePasswordChange(state, value) {
|
|
35
|
+
state.forcePasswordChange = value;
|
|
36
|
+
}
|
|
33
37
|
},
|
|
34
38
|
|
|
35
39
|
actions: {
|
|
@@ -42,6 +46,7 @@ const store = {
|
|
|
42
46
|
})
|
|
43
47
|
.then((res) => {
|
|
44
48
|
commit("setUser", res.data.data);
|
|
49
|
+
commit("setForcePasswordChange",res.data.data?.force_password_change);
|
|
45
50
|
return res.data.data;
|
|
46
51
|
})
|
|
47
52
|
.catch(errors);
|
|
@@ -50,6 +55,7 @@ const store = {
|
|
|
50
55
|
logout({ commit }, { errors }) {
|
|
51
56
|
commit("setAuth", false);
|
|
52
57
|
commit("setUser", null);
|
|
58
|
+
commit("setForcePasswordChange", false);
|
|
53
59
|
|
|
54
60
|
return axios.post("/logout");
|
|
55
61
|
},
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
axios.interceptors.request.use(
|
|
5
|
+
request => {
|
|
6
|
+
throw { isLocal: true, data: retrieveResponse(request) };
|
|
7
|
+
},
|
|
8
|
+
error => {
|
|
9
|
+
return Promise.resolve(error);
|
|
10
|
+
}
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
axios.interceptors.response.use(
|
|
14
|
+
response => {
|
|
15
|
+
return response;
|
|
16
|
+
},
|
|
17
|
+
error => {
|
|
18
|
+
return Promise.resolve(error);
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function retrieveResponse(request) {
|
|
23
|
+
return fakes[request.url][request.method];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fakes = {
|
|
27
|
+
"/api/user/self": {
|
|
28
|
+
"get" : {
|
|
29
|
+
"data":{
|
|
30
|
+
"id": 2,
|
|
31
|
+
"company_id": 1,
|
|
32
|
+
"created_at": "2025-04-14T15:11:11.000000Z",
|
|
33
|
+
"updated_at": "2025-04-16T16:45:55.000000Z",
|
|
34
|
+
"newsletter": null,
|
|
35
|
+
"admin": 1,
|
|
36
|
+
"name": "Jeremy Viner",
|
|
37
|
+
"email": "jeremy.viner@avalerehealth.com",
|
|
38
|
+
"email_verified_at": "2025-04-16T16:45:55.000000Z",
|
|
39
|
+
"brands": [],
|
|
40
|
+
"company": {
|
|
41
|
+
"id": 1,
|
|
42
|
+
"name": "Company 0",
|
|
43
|
+
"created_at": "2025-04-14T15:08:50.000000Z",
|
|
44
|
+
"updated_at": "2025-04-14T15:08:50.000000Z",
|
|
45
|
+
"domains": [
|
|
46
|
+
"okuneva.com",
|
|
47
|
+
"bogisich.info",
|
|
48
|
+
"avalerehealth.com"
|
|
49
|
+
],
|
|
50
|
+
"brands": [
|
|
51
|
+
1
|
|
52
|
+
],
|
|
53
|
+
"events": [],
|
|
54
|
+
"therapy_areas": [],
|
|
55
|
+
"event_therapy_areas": [],
|
|
56
|
+
"event_therapy_area_media_types": [],
|
|
57
|
+
"users_count": null,
|
|
58
|
+
"sso_client_id": "5ea409fc-8dcf-423e-b7fa-867422859ea4",
|
|
59
|
+
"sso_client_secret": "aSS8Q~ss0r-uh.fPLHUXVXp2kIs5IfwTjzomrb_I",
|
|
60
|
+
"sso_tenant": "4a33c544-865e-44a4-836f-bc51800f6c5e",
|
|
61
|
+
"sso_type": "azure"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"/user/password": {
|
|
67
|
+
"put": {
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"/logged-in": {
|
|
72
|
+
"get":{"logged-in":true},
|
|
73
|
+
},
|
|
74
|
+
"/logout": {
|
|
75
|
+
"post": {
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"/login" : {
|
|
80
|
+
"post": {
|
|
81
|
+
"two_factor": false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<section id="resetPasswordForm">
|
|
4
|
+
<h1 class="h2 h2--small" v-html="!form.successful ? 'Change password' : 'Success'" />
|
|
5
|
+
<form class="form" @submit.prevent="submit">
|
|
6
|
+
<div v-if="!form.successful">
|
|
7
|
+
<p class="AM-mt-2 AM-mb-0">
|
|
8
|
+
Please complete the fields below to change your password.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<el-input
|
|
12
|
+
class="AM-mt-3"
|
|
13
|
+
label="Current password"
|
|
14
|
+
placeholder="Enter your current password"
|
|
15
|
+
name="current_password"
|
|
16
|
+
type="password"
|
|
17
|
+
required
|
|
18
|
+
v-model="form.current_password"
|
|
19
|
+
:error="form.errors"
|
|
20
|
+
/>
|
|
21
|
+
<el-input
|
|
22
|
+
v-model="form.password"
|
|
23
|
+
class="AM-mt-2 AM-mb-2"
|
|
24
|
+
label="New Password"
|
|
25
|
+
placeholder="Enter your new password"
|
|
26
|
+
name="password"
|
|
27
|
+
:error="form.errors"
|
|
28
|
+
type="password"
|
|
29
|
+
autocomplete="new-password"
|
|
30
|
+
required
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<VPasswordValidation :password="form.password" @passwordValid="updatePasswordValidity" />
|
|
34
|
+
<div class="flex AM-mt-3">
|
|
35
|
+
<elButton
|
|
36
|
+
class=""
|
|
37
|
+
type="primary"
|
|
38
|
+
:disabled="form.processing || !isPasswordValid"
|
|
39
|
+
:loading="form.processing"
|
|
40
|
+
@click="onSubmit"
|
|
41
|
+
>
|
|
42
|
+
<span v-text="'Change password'" />
|
|
43
|
+
</elButton>
|
|
44
|
+
|
|
45
|
+
<elButton
|
|
46
|
+
class=""
|
|
47
|
+
type="secondary"
|
|
48
|
+
@click="handleButton"
|
|
49
|
+
>
|
|
50
|
+
<span v-text="'Cancel'" />
|
|
51
|
+
</elButton>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div v-else>
|
|
55
|
+
<strong class="">Email: {{ $store.state.auth?.user?.email }}</strong>
|
|
56
|
+
<p v-text="`Your password has been updated.`" />
|
|
57
|
+
<elButton
|
|
58
|
+
class=""
|
|
59
|
+
type="secondary"
|
|
60
|
+
@click="handleButton"
|
|
61
|
+
>
|
|
62
|
+
<span v-text="'Continue'" />
|
|
63
|
+
</elButton>
|
|
64
|
+
</div>
|
|
65
|
+
</form>
|
|
66
|
+
</section>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script>
|
|
71
|
+
import Form from "form-backend-validation";
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
data() {
|
|
75
|
+
return {
|
|
76
|
+
form: new Form(
|
|
77
|
+
{
|
|
78
|
+
email: this.$store.state.auth.user?.email,
|
|
79
|
+
password: '',
|
|
80
|
+
current_password: ''
|
|
81
|
+
},
|
|
82
|
+
{ resetOnSuccess: false }
|
|
83
|
+
),
|
|
84
|
+
isPasswordValid: false,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
mounted() {
|
|
89
|
+
|
|
90
|
+
this.$store.dispatch("getUser", {
|
|
91
|
+
errors: this.$root.errors,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
methods: {
|
|
96
|
+
async onSubmit() {
|
|
97
|
+
this.loading = true;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await this.form.put('/user/password');
|
|
101
|
+
await this.login();
|
|
102
|
+
} catch (e) {
|
|
103
|
+
this.$root.errors(e);
|
|
104
|
+
} finally {
|
|
105
|
+
this.loading = false;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
async login() {
|
|
109
|
+
this.loading = true;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const res = await this.form.post("/login");
|
|
113
|
+
|
|
114
|
+
if(res['logged-in']){
|
|
115
|
+
try{
|
|
116
|
+
await this.$store.dispatch("logout", {
|
|
117
|
+
// errors: this.$root.errors,
|
|
118
|
+
});
|
|
119
|
+
} catch(e){}
|
|
120
|
+
|
|
121
|
+
await this.form.post("/login");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
} catch (e) {
|
|
125
|
+
this.$root.errors(e);
|
|
126
|
+
} finally {
|
|
127
|
+
this.loading = false;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
updatePasswordValidity(isValid) {
|
|
132
|
+
this.isPasswordValid = isValid;
|
|
133
|
+
},
|
|
134
|
+
handleButton() {
|
|
135
|
+
this.$emit('close');
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
metaInfo() {
|
|
140
|
+
return {
|
|
141
|
+
title: "Reset Password",
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
components: {
|
|
146
|
+
VPasswordValidation: require("./../components/VPasswordValidation.vue").default,
|
|
147
|
+
elInput: require('../../form/basic.vue').default,
|
|
148
|
+
elButton: require('../../basic/Button.vue').default,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
</script>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative">
|
|
3
3
|
<section id="resetPasswordForm">
|
|
4
|
-
<h1 class="h2"
|
|
4
|
+
<h1 class="h2 h2--small" v-html="!form.successful ? 'Welcome' : 'Success'" />
|
|
5
5
|
<form class="form" @submit.prevent="submit">
|
|
6
6
|
<div v-if="!form.successful">
|
|
7
7
|
<p class="AM-mt-2 AM-mb-0 AM-color-highlight">
|
|
@@ -35,17 +35,14 @@
|
|
|
35
35
|
</elButton>
|
|
36
36
|
</div>
|
|
37
37
|
<div v-else>
|
|
38
|
-
<
|
|
39
|
-
<strong class="">Email: {{ user?.email }}</strong>
|
|
38
|
+
<strong class="">Email: {{ $store.state.auth?.user?.email }}</strong>
|
|
40
39
|
<p v-text="`Your password has been updated.`" />
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</router-link>
|
|
48
|
-
</p>
|
|
40
|
+
<elButton
|
|
41
|
+
type="primary"
|
|
42
|
+
@click="handleButton"
|
|
43
|
+
>
|
|
44
|
+
Continue
|
|
45
|
+
</elButton>
|
|
49
46
|
</div>
|
|
50
47
|
</form>
|
|
51
48
|
</section>
|
|
@@ -54,7 +51,6 @@
|
|
|
54
51
|
|
|
55
52
|
<script>
|
|
56
53
|
import Form from "form-backend-validation";
|
|
57
|
-
import store from "../js/AuthStore";
|
|
58
54
|
|
|
59
55
|
export default {
|
|
60
56
|
data() {
|
|
@@ -63,6 +59,7 @@ export default {
|
|
|
63
59
|
{
|
|
64
60
|
email: this.$store.state.auth.user?.email,
|
|
65
61
|
password: '',
|
|
62
|
+
otp: true,
|
|
66
63
|
},
|
|
67
64
|
{ resetOnSuccess: false }
|
|
68
65
|
),
|
|
@@ -105,7 +102,6 @@ export default {
|
|
|
105
102
|
|
|
106
103
|
await this.form.post("/login");
|
|
107
104
|
}
|
|
108
|
-
this.$router.push({name: `${this.$store.state.auth.authBase}.callback`,query: {authenticated: true}});
|
|
109
105
|
|
|
110
106
|
} catch (e) {
|
|
111
107
|
this.$root.errors(e);
|
|
@@ -117,6 +113,9 @@ export default {
|
|
|
117
113
|
updatePasswordValidity(isValid) {
|
|
118
114
|
this.isPasswordValid = isValid;
|
|
119
115
|
},
|
|
116
|
+
handleButton() {
|
|
117
|
+
this.$emit('close');
|
|
118
|
+
}
|
|
120
119
|
},
|
|
121
120
|
|
|
122
121
|
metaInfo() {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
<el-button
|
|
30
30
|
type="primary"
|
|
31
|
-
:disabled="loading || (form.email
|
|
31
|
+
:disabled="loading || (!isValidEmail(form.email))"
|
|
32
32
|
@click="onSubmit"
|
|
33
33
|
>
|
|
34
34
|
Continue
|
|
@@ -112,7 +112,12 @@ export default {
|
|
|
112
112
|
vue.setCountdown();
|
|
113
113
|
},1000);
|
|
114
114
|
}
|
|
115
|
+
},
|
|
116
|
+
isValidEmail(email) {
|
|
117
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
118
|
+
return emailRegex.test(email);
|
|
115
119
|
}
|
|
120
|
+
|
|
116
121
|
},
|
|
117
122
|
|
|
118
123
|
mounted() {
|
package/modules/_AuthModule.scss
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@import "@fishawack/lab-ui/_modal.scss";
|
|
2
|
+
|
|
3
|
+
.modal--auth {
|
|
4
|
+
background-color: transparent;
|
|
5
|
+
|
|
6
|
+
transition: all 500ms ease-in-out;
|
|
7
|
+
|
|
8
|
+
&::before{
|
|
9
|
+
content: '';
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
z-index: 0 !important;
|
|
12
|
+
position: fixed;
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
background: #00000066;
|
|
16
|
+
backdrop-filter: blur(1.5rem);
|
|
17
|
+
top: 0;
|
|
18
|
+
left: 0;
|
|
19
|
+
right: 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
.AuthModule__form.modal__box {
|
|
23
|
+
position: absolute;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fishawack/lab-velocity",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Avalere Health branded style system",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"setup": "npm ci || npm i && npm run content",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"vue-loader": "^17.2.2",
|
|
39
39
|
"vue-router": "^4.4.0",
|
|
40
40
|
"vuex": "^4.1.0",
|
|
41
|
-
"vuex-persistedstate": "^4.1.0"
|
|
41
|
+
"vuex-persistedstate": "^4.1.0",
|
|
42
|
+
"form-backend-validation": "github:mikemellor11/form-backend-validation#master",
|
|
43
|
+
"mitt": "^3.0.1"
|
|
42
44
|
},
|
|
43
45
|
"dependencies": {
|
|
44
46
|
"@tiptap/extension-link": "^2.11.2",
|
|
@@ -53,7 +55,6 @@
|
|
|
53
55
|
"@tiptap/starter-kit": "^2.11.2",
|
|
54
56
|
"@tiptap/vue-3": "^2.11.2",
|
|
55
57
|
"element-plus": "^2.7.8",
|
|
56
|
-
"form-backend-validation": "github:mikemellor11/form-backend-validation#master",
|
|
57
58
|
"quill": "^1.3.7",
|
|
58
59
|
"sanitize-html": "^2.13.1"
|
|
59
60
|
},
|