@fishawack/lab-velocity 2.0.0-beta.44 → 2.0.0-beta.45
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/README.md +25 -0
- package/_Build/vue/components/layout/PageHeader.vue +7 -6
- package/_Build/vue/components/layout/Table.vue +5 -0
- package/_Build/vue/components/layout/TableSorter.vue +1 -0
- package/_Build/vue/modules/AuthModule/js/guest-request.js +32 -0
- package/_Build/vue/modules/AuthModule/js/impersonation-banner.js +102 -0
- package/_Build/vue/modules/AuthModule/js/router.js +61 -27
- package/_Build/vue/modules/AuthModule/js/store.js +8 -0
- package/_Build/vue/modules/AuthModule/routes/PCompanies/resource.js +2 -3
- package/_Build/vue/modules/AuthModule/routes/PTeams/resource.js +1 -0
- package/_Build/vue/modules/AuthModule/routes/PUsers/SetPasswordAction.vue +51 -0
- package/_Build/vue/modules/AuthModule/routes/PUsers/SetPasswordDialog.vue +138 -0
- package/_Build/vue/modules/AuthModule/routes/PUsers/resource.js +39 -2
- package/_Build/vue/modules/AuthModule/routes/change-password.vue +6 -9
- package/_Build/vue/modules/AuthModule/routes/force-reset.vue +6 -9
- package/_Build/vue/modules/AuthModule/routes/forgot.vue +6 -1
- package/_Build/vue/modules/AuthModule/routes/login.vue +6 -9
- package/_Build/vue/modules/AuthModule/routes/loginsso.vue +7 -1
- package/_Build/vue/modules/AuthModule/routes/logout.vue +10 -2
- package/_Build/vue/modules/AuthModule/routes/register.vue +6 -8
- package/_Build/vue/modules/AuthModule/routes/reset.vue +6 -1
- package/_Build/vue/modules/AuthModule/routes/success-forgot.vue +6 -1
- package/_Build/vue/modules/resource/index.js +9 -1
- package/_Build/vue/modules/resource/trashable.js +104 -0
- package/components/_table.scss +3 -0
- package/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ import { Auth } from "@fishawack/lab-velocity";
|
|
|
103
103
|
// ...
|
|
104
104
|
paths: ["auth.user"],
|
|
105
105
|
}),
|
|
106
|
+
Auth.ImpersonationPlugin,
|
|
106
107
|
],
|
|
107
108
|
|
|
108
109
|
modules: {
|
|
@@ -113,6 +114,8 @@ import { Auth } from "@fishawack/lab-velocity";
|
|
|
113
114
|
}
|
|
114
115
|
```
|
|
115
116
|
|
|
117
|
+
`Auth.ImpersonationPlugin` displays a persistent banner when an admin is impersonating another user, persisted across page reloads.
|
|
118
|
+
|
|
116
119
|
### Base Styles
|
|
117
120
|
|
|
118
121
|
@fishawack/lab-velocity extends @fishawack/lab-ui, for this reason you should replace the two references to variables & defaults with @fishawack/lab-velocity ones.
|
|
@@ -188,6 +191,7 @@ There are two different set of sass imports for the admin and the frontend route
|
|
|
188
191
|
@import "@fishawack/lab-velocity/components/menu";
|
|
189
192
|
@import "@fishawack/lab-velocity/components/layout";
|
|
190
193
|
@import "@fishawack/lab-velocity/components/descriptions";
|
|
194
|
+
@import "@fishawack/lab-velocity/components/token-display";
|
|
191
195
|
@import "element-plus/theme-chalk/el-tabs";
|
|
192
196
|
@import "element-plus/theme-chalk/el-tab-pane";
|
|
193
197
|
```
|
|
@@ -478,3 +482,24 @@ import { Checkbox as VelCheckbox } from "@fishawack/lab-velocity";
|
|
|
478
482
|
),
|
|
479
483
|
},
|
|
480
484
|
```
|
|
485
|
+
|
|
486
|
+
## Local dev
|
|
487
|
+
|
|
488
|
+
To work locally mount a local clone of the project into the packages directory via an overridden docker compose file.
|
|
489
|
+
|
|
490
|
+
### Docker setup
|
|
491
|
+
|
|
492
|
+
Create a docker compose file at `_Docker/docker-compose.yml`
|
|
493
|
+
|
|
494
|
+
```yml
|
|
495
|
+
services:
|
|
496
|
+
core:
|
|
497
|
+
volumes:
|
|
498
|
+
- $PWD/../lab-velocity:/app/packages/lab-velocity
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Npm install
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
npm install ./packages/lab-velocity
|
|
505
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="
|
|
3
|
-
<div class="
|
|
2
|
+
<div class="flex flex-wrap items-start gap" style="row-gap: 16px">
|
|
3
|
+
<div class="flex justify-start items-center">
|
|
4
4
|
<span v-if="icon">
|
|
5
5
|
<div
|
|
6
6
|
class="p-1.5 mr-2 border-radius border border-solid border-muted flex items-center justify-center"
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
<h2 class="m-0 font-500 text-secondary">{{ title }}</h2>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
|
-
<div
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
<div
|
|
21
|
+
class="flex flex-wrap gap items-center justify-end"
|
|
22
|
+
style="margin-left: auto"
|
|
23
|
+
>
|
|
24
|
+
<slot />
|
|
24
25
|
</div>
|
|
25
26
|
</div>
|
|
26
27
|
</template>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<el-table
|
|
3
3
|
:data="$props.data"
|
|
4
4
|
:height="fixedHeight ? 762 : undefined"
|
|
5
|
+
:row-class-name="rowClassName"
|
|
5
6
|
style="width: 100%"
|
|
6
7
|
@sort-change="handleSort"
|
|
7
8
|
>
|
|
@@ -131,6 +132,10 @@ export default {
|
|
|
131
132
|
type: Boolean,
|
|
132
133
|
default: true,
|
|
133
134
|
},
|
|
135
|
+
rowClassName: {
|
|
136
|
+
type: [Function, String],
|
|
137
|
+
default: undefined,
|
|
138
|
+
},
|
|
134
139
|
},
|
|
135
140
|
emits: ["sort", "reload"],
|
|
136
141
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a form POST to a guest-only route with automatic logged-in detection.
|
|
6
|
+
*
|
|
7
|
+
* If the server redirects to /hydrate/logged-in (because a stale session
|
|
8
|
+
* exists), the helper dispatches a logout, fetches a fresh CSRF cookie,
|
|
9
|
+
* and retries the original request once.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} options
|
|
12
|
+
* @param {object} options.form - form-backend-validation Form instance
|
|
13
|
+
* @param {string} options.url - endpoint to POST to
|
|
14
|
+
* @param {string} [options.method] - HTTP method (default: "post")
|
|
15
|
+
* @param {import('vuex').Store} options.store - Vuex store (for dispatch("logout"))
|
|
16
|
+
* @returns {Promise<object>} resolved response data
|
|
17
|
+
*/
|
|
18
|
+
export async function guestRequest({ form, url, method = "post", store }) {
|
|
19
|
+
const res = await form[method](url);
|
|
20
|
+
|
|
21
|
+
if (res && res["logged-in"]) {
|
|
22
|
+
try {
|
|
23
|
+
await store.dispatch("logout");
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
|
|
26
|
+
await axios.get("/sanctum/csrf-cookie");
|
|
27
|
+
|
|
28
|
+
return await form[method](url);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return res;
|
|
32
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { createApp, h, ref } from "vue";
|
|
4
|
+
|
|
5
|
+
let bannerApp = null;
|
|
6
|
+
let bannerContainer = null;
|
|
7
|
+
let storeRef = null;
|
|
8
|
+
const currentUser = ref(null);
|
|
9
|
+
|
|
10
|
+
function mountBanner() {
|
|
11
|
+
if (bannerApp) return;
|
|
12
|
+
|
|
13
|
+
bannerContainer = document.createElement("div");
|
|
14
|
+
document.body.insertBefore(bannerContainer, document.body.firstChild);
|
|
15
|
+
|
|
16
|
+
bannerApp = createApp({
|
|
17
|
+
setup() {
|
|
18
|
+
function stop() {
|
|
19
|
+
storeRef.dispatch("stopImpersonating");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return () =>
|
|
23
|
+
h(
|
|
24
|
+
"div",
|
|
25
|
+
{
|
|
26
|
+
style: {
|
|
27
|
+
background: "#c0392b",
|
|
28
|
+
color: "#fff",
|
|
29
|
+
display: "flex",
|
|
30
|
+
alignItems: "center",
|
|
31
|
+
justifyContent: "center",
|
|
32
|
+
gap: "16px",
|
|
33
|
+
padding: "12px 16px",
|
|
34
|
+
fontSize: "14px",
|
|
35
|
+
fontFamily: "sans-serif",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
[
|
|
39
|
+
h(
|
|
40
|
+
"span",
|
|
41
|
+
`Impersonating: ${currentUser.value?.name} (${currentUser.value?.email})`,
|
|
42
|
+
),
|
|
43
|
+
h(
|
|
44
|
+
"button",
|
|
45
|
+
{
|
|
46
|
+
onClick: stop,
|
|
47
|
+
style: {
|
|
48
|
+
background: "#fff",
|
|
49
|
+
color: "#c0392b",
|
|
50
|
+
border: "none",
|
|
51
|
+
padding: "4px 12px",
|
|
52
|
+
borderRadius: "4px",
|
|
53
|
+
cursor: "pointer",
|
|
54
|
+
fontWeight: "bold",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"Stop impersonating",
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
bannerApp.mount(bannerContainer);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unmountBanner() {
|
|
68
|
+
if (!bannerApp) return;
|
|
69
|
+
|
|
70
|
+
bannerApp.unmount();
|
|
71
|
+
bannerContainer?.remove();
|
|
72
|
+
bannerApp = null;
|
|
73
|
+
bannerContainer = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function syncImpersonationBanner(user) {
|
|
77
|
+
currentUser.value = user;
|
|
78
|
+
|
|
79
|
+
if (user?.impersonating_as) {
|
|
80
|
+
mountBanner();
|
|
81
|
+
} else {
|
|
82
|
+
unmountBanner();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function setImpersonationStore(store) {
|
|
87
|
+
storeRef = store;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ImpersonationPlugin(store) {
|
|
91
|
+
setImpersonationStore(store);
|
|
92
|
+
|
|
93
|
+
// Handle persisted state on load (vuex-persistedstate bypasses mutations)
|
|
94
|
+
syncImpersonationBanner(store.state.auth?.user ?? store.state.user);
|
|
95
|
+
|
|
96
|
+
// Handle all future setUser mutations
|
|
97
|
+
store.subscribe((mutation) => {
|
|
98
|
+
if (mutation.type === "auth/setUser" || mutation.type === "setUser") {
|
|
99
|
+
syncImpersonationBanner(mutation.payload);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -179,50 +179,84 @@ export function routes(node) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
export function beforeEach(router, store) {
|
|
182
|
+
let initialLoad = true;
|
|
183
|
+
|
|
184
|
+
// These routes must always be reachable regardless of auth/verification state.
|
|
185
|
+
// Without this, force_password_change or unverified state can trap the user with
|
|
186
|
+
// no way to log out or complete an SSO callback.
|
|
187
|
+
const ALWAYS_ALLOW = new Set(["auth.logout", "auth.callback"]);
|
|
188
|
+
|
|
182
189
|
router.beforeEach(async (to, from, next) => {
|
|
183
|
-
//
|
|
184
|
-
|
|
190
|
+
// Refresh user state on initial page load (handles cross-app state sync)
|
|
191
|
+
// or when authenticated query param is present (bypass/redirect flow)
|
|
192
|
+
if (
|
|
193
|
+
(store.getters.authenticated && initialLoad) ||
|
|
194
|
+
to.query.authenticated
|
|
195
|
+
) {
|
|
185
196
|
await store.dispatch("getUser", {
|
|
186
197
|
errors: (e) => console.error(e),
|
|
187
198
|
});
|
|
188
199
|
}
|
|
189
200
|
|
|
201
|
+
initialLoad = false;
|
|
202
|
+
|
|
190
203
|
const { user, redirect } = store.state.auth;
|
|
204
|
+
const isGuestRoute = to.matched.some((d) => d.meta.guest) === true;
|
|
191
205
|
|
|
192
|
-
//
|
|
206
|
+
// Logout and SSO callback must always be reachable — prevents every
|
|
207
|
+
// possible "stuck" scenario caused by state-based redirects below.
|
|
208
|
+
if (ALWAYS_ALLOW.has(to.name)) {
|
|
209
|
+
return next();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Redirect already-verified users away from verification routes
|
|
193
213
|
if (
|
|
194
214
|
to.query.verified ||
|
|
195
215
|
(to.name === "auth.expired-verification" && user?.email_verified_at)
|
|
196
216
|
) {
|
|
197
|
-
next({ name: "auth.success-verify" });
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
return next({ name: "auth.success-verify" });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (store.getters.authenticated) {
|
|
221
|
+
// 1. Email verification is the highest priority redirect.
|
|
222
|
+
// Must come before force_password_change — there is no point forcing
|
|
223
|
+
// a password change on an account that hasn't been verified yet.
|
|
224
|
+
// Guest routes, auth.verify and auth.expired-verification are exempt
|
|
225
|
+
// so the user can always complete or re-request verification.
|
|
226
|
+
if (
|
|
207
227
|
!user?.email_verified_at &&
|
|
208
|
-
|
|
228
|
+
!isGuestRoute &&
|
|
229
|
+
to.name !== "auth.verify" &&
|
|
230
|
+
to.name !== "auth.expired-verification"
|
|
209
231
|
) {
|
|
210
|
-
|
|
211
|
-
next({ name: "auth.verify" });
|
|
212
|
-
} else {
|
|
213
|
-
// User is authenticated and authorized - proceed
|
|
214
|
-
next();
|
|
232
|
+
return next({ name: "auth.verify" });
|
|
215
233
|
}
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
|
|
235
|
+
// 2. Force password change applies only after verification is satisfied.
|
|
236
|
+
// Guest routes are exempt so the user can still navigate within /auth
|
|
237
|
+
// (e.g. auth.verify, auth.expired-verification) without looping.
|
|
238
|
+
if (
|
|
239
|
+
user?.force_password_change &&
|
|
240
|
+
!isGuestRoute &&
|
|
241
|
+
to.name !== "auth.force-reset"
|
|
242
|
+
) {
|
|
243
|
+
return next({ name: "auth.force-reset" });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 3. Redirect an already-authenticated user away from the login page
|
|
247
|
+
if (to.name === "auth.login") {
|
|
248
|
+
return next({ name: redirect });
|
|
224
249
|
}
|
|
250
|
+
|
|
251
|
+
return next();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Unauthenticated: allow guest routes, otherwise redirect to login
|
|
255
|
+
if (isGuestRoute) {
|
|
256
|
+
return next();
|
|
225
257
|
}
|
|
258
|
+
|
|
259
|
+
return next({ name: "auth.login" });
|
|
226
260
|
});
|
|
227
261
|
}
|
|
228
262
|
|
|
@@ -65,6 +65,14 @@ const store = {
|
|
|
65
65
|
|
|
66
66
|
return axios.post("/logout");
|
|
67
67
|
},
|
|
68
|
+
|
|
69
|
+
stopImpersonating({ commit }) {
|
|
70
|
+
return axios.post("/api/users/impersonate/stop").then((res) => {
|
|
71
|
+
commit("setUser", res.data.data);
|
|
72
|
+
|
|
73
|
+
return res.data.data;
|
|
74
|
+
});
|
|
75
|
+
},
|
|
68
76
|
},
|
|
69
77
|
};
|
|
70
78
|
|
|
@@ -19,9 +19,7 @@ export default [
|
|
|
19
19
|
{
|
|
20
20
|
api: {
|
|
21
21
|
params: {
|
|
22
|
-
index: (
|
|
23
|
-
"filter[withTrashed]": $route.query.trashed,
|
|
24
|
-
}),
|
|
22
|
+
index: () => ({}),
|
|
25
23
|
show: () => ({
|
|
26
24
|
include: "primary_contact",
|
|
27
25
|
}),
|
|
@@ -35,6 +33,7 @@ export default [
|
|
|
35
33
|
singular: "company",
|
|
36
34
|
icon: "icon-cases",
|
|
37
35
|
auditable: true,
|
|
36
|
+
trashable: true,
|
|
38
37
|
...merge(columns(companiesColumns), {
|
|
39
38
|
index: {
|
|
40
39
|
layout: [
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<vel-button @click="visible = true">Set Password</vel-button>
|
|
3
|
+
|
|
4
|
+
<SetPasswordDialog
|
|
5
|
+
v-if="visible"
|
|
6
|
+
:user-id="userId"
|
|
7
|
+
@close="visible = false"
|
|
8
|
+
@success="onSuccess"
|
|
9
|
+
/>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
import VelButton from "../../../../components/basic/Button.vue";
|
|
14
|
+
import SetPasswordDialog from "./SetPasswordDialog.vue";
|
|
15
|
+
import { ElNotification } from "element-plus";
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
components: {
|
|
19
|
+
VelButton,
|
|
20
|
+
SetPasswordDialog,
|
|
21
|
+
},
|
|
22
|
+
props: {
|
|
23
|
+
userId: {
|
|
24
|
+
type: [Number, String],
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
model: {
|
|
28
|
+
type: Object,
|
|
29
|
+
required: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
data() {
|
|
33
|
+
return {
|
|
34
|
+
visible: false,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
methods: {
|
|
38
|
+
onSuccess(data) {
|
|
39
|
+
this.visible = false;
|
|
40
|
+
|
|
41
|
+
Object.assign(this.model, data);
|
|
42
|
+
|
|
43
|
+
ElNotification({
|
|
44
|
+
title: "Success",
|
|
45
|
+
message: "Password updated",
|
|
46
|
+
type: "success",
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
</script>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog
|
|
3
|
+
:model-value="true"
|
|
4
|
+
title="Set Password"
|
|
5
|
+
width="500px"
|
|
6
|
+
@close="$emit('close')"
|
|
7
|
+
>
|
|
8
|
+
<vel-basic
|
|
9
|
+
v-model="password"
|
|
10
|
+
name="password"
|
|
11
|
+
type="password"
|
|
12
|
+
label="Password"
|
|
13
|
+
placeholder="Enter new password"
|
|
14
|
+
class="AM-mb-2"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<vel-basic
|
|
18
|
+
v-model="passwordConfirmation"
|
|
19
|
+
name="password_confirmation"
|
|
20
|
+
type="password"
|
|
21
|
+
label="Confirm Password"
|
|
22
|
+
placeholder="Confirm new password"
|
|
23
|
+
class="AM-mb-2"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<div class="AM-mb-2">
|
|
27
|
+
<p class="font-700 AM-mb-0.5">Password must contain:</p>
|
|
28
|
+
<p class="m-0">
|
|
29
|
+
{{ passwordLengthValid ? "✓" : "✗" }} At least 8 characters
|
|
30
|
+
</p>
|
|
31
|
+
<p class="m-0">{{ hasLetter ? "✓" : "✗" }} At least one letter</p>
|
|
32
|
+
<p class="m-0">
|
|
33
|
+
{{ hasNumberOrSymbol ? "✓" : "✗" }} At least one number or
|
|
34
|
+
symbol
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<template #footer>
|
|
39
|
+
<el-button @click="$emit('close')">Cancel</el-button>
|
|
40
|
+
<el-button
|
|
41
|
+
type="primary"
|
|
42
|
+
:disabled="!canSubmit"
|
|
43
|
+
:loading="loading"
|
|
44
|
+
@click="submit"
|
|
45
|
+
>
|
|
46
|
+
Set Password
|
|
47
|
+
</el-button>
|
|
48
|
+
</template>
|
|
49
|
+
</el-dialog>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
import { ElDialog, ElButton, ElNotification } from "element-plus";
|
|
54
|
+
import VelBasic from "../../../../components/form/basic.vue";
|
|
55
|
+
import axios from "axios";
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
components: {
|
|
59
|
+
ElDialog,
|
|
60
|
+
ElButton,
|
|
61
|
+
VelBasic,
|
|
62
|
+
},
|
|
63
|
+
props: {
|
|
64
|
+
userId: {
|
|
65
|
+
type: [Number, String],
|
|
66
|
+
required: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
emits: ["close", "success"],
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
password: "",
|
|
73
|
+
passwordConfirmation: "",
|
|
74
|
+
loading: false,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
computed: {
|
|
78
|
+
passwordLengthValid() {
|
|
79
|
+
return this.password.length >= 8;
|
|
80
|
+
},
|
|
81
|
+
hasLetter() {
|
|
82
|
+
return /[a-zA-Z]/.test(this.password);
|
|
83
|
+
},
|
|
84
|
+
hasNumberOrSymbol() {
|
|
85
|
+
return (
|
|
86
|
+
/[0-9]/.test(this.password) ||
|
|
87
|
+
/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(this.password)
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
passwordValid() {
|
|
91
|
+
return (
|
|
92
|
+
this.passwordLengthValid &&
|
|
93
|
+
this.hasLetter &&
|
|
94
|
+
this.hasNumberOrSymbol
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
canSubmit() {
|
|
98
|
+
return (
|
|
99
|
+
this.passwordValid &&
|
|
100
|
+
this.password === this.passwordConfirmation &&
|
|
101
|
+
this.password.length > 0
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
methods: {
|
|
106
|
+
async submit() {
|
|
107
|
+
this.loading = true;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const res = await axios.post(
|
|
111
|
+
`/api/users/${this.userId}/set-password`,
|
|
112
|
+
{
|
|
113
|
+
password: this.password,
|
|
114
|
+
password_confirmation: this.passwordConfirmation,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
this.$emit("success", res.data.data);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e.response?.status === 422) {
|
|
121
|
+
const errors = e.response.data.errors || {};
|
|
122
|
+
const message =
|
|
123
|
+
Object.values(errors).flat().join("\n") ||
|
|
124
|
+
e.response.data.message;
|
|
125
|
+
|
|
126
|
+
ElNotification({
|
|
127
|
+
title: "Validation Error",
|
|
128
|
+
message,
|
|
129
|
+
type: "error",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
this.loading = false;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
</script>
|
|
@@ -9,6 +9,7 @@ import usersColumns, { detectedCompany } from "./columns.js";
|
|
|
9
9
|
import VelFormRole from "../../../../components/layout/FormRole.vue";
|
|
10
10
|
import VelButton from "../../../../components/basic/Button.vue";
|
|
11
11
|
import VelRoleLegend from "../../../../components/layout/RoleLegend.vue";
|
|
12
|
+
import SetPasswordAction from "./SetPasswordAction.vue";
|
|
12
13
|
|
|
13
14
|
function generatePassword(
|
|
14
15
|
length = 20,
|
|
@@ -26,9 +27,8 @@ export default [
|
|
|
26
27
|
{
|
|
27
28
|
api: {
|
|
28
29
|
params: {
|
|
29
|
-
index: (
|
|
30
|
+
index: () => ({
|
|
30
31
|
include: "company",
|
|
31
|
-
"filter[withTrashed]": $route.query.trashed,
|
|
32
32
|
}),
|
|
33
33
|
show: () => ({ include: "company" }),
|
|
34
34
|
},
|
|
@@ -37,6 +37,7 @@ export default [
|
|
|
37
37
|
value: "email",
|
|
38
38
|
},
|
|
39
39
|
auditable: true,
|
|
40
|
+
trashable: true,
|
|
40
41
|
permissions: {
|
|
41
42
|
create: ({ $store }) => $store.getters.can("write users"),
|
|
42
43
|
edit: ({ $store }) => $store.getters.can("write users"),
|
|
@@ -152,6 +153,42 @@ export default [
|
|
|
152
153
|
},
|
|
153
154
|
show: {
|
|
154
155
|
actions: [
|
|
156
|
+
({ model, $store }) =>
|
|
157
|
+
$store.getters.can("write users") &&
|
|
158
|
+
!model.email_verified_at &&
|
|
159
|
+
h(
|
|
160
|
+
VelButton,
|
|
161
|
+
{
|
|
162
|
+
async onClick() {
|
|
163
|
+
try {
|
|
164
|
+
const res = await axios.post(
|
|
165
|
+
`/api/users/${model.id}/verify`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
Object.assign(model, res.data.data);
|
|
169
|
+
|
|
170
|
+
ElNotification({
|
|
171
|
+
title: "Success",
|
|
172
|
+
message: "User marked as verified",
|
|
173
|
+
type: "success",
|
|
174
|
+
});
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.log(e);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
"Mark as Verified",
|
|
181
|
+
),
|
|
182
|
+
({ model, $store }) => {
|
|
183
|
+
if (!$store.getters.can("write users")) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return h(SetPasswordAction, {
|
|
188
|
+
userId: model.id,
|
|
189
|
+
model,
|
|
190
|
+
});
|
|
191
|
+
},
|
|
155
192
|
({ model, $store, $root }) =>
|
|
156
193
|
$store.getters.can("impersonate users") &&
|
|
157
194
|
h(
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
|
|
82
82
|
<script>
|
|
83
83
|
import Form from "form-backend-validation";
|
|
84
|
+
import { guestRequest } from "../js/guest-request";
|
|
84
85
|
|
|
85
86
|
export default {
|
|
86
87
|
components: {
|
|
@@ -126,15 +127,11 @@ export default {
|
|
|
126
127
|
this.loading = true;
|
|
127
128
|
|
|
128
129
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
} catch (e) {}
|
|
135
|
-
|
|
136
|
-
await this.form.post("/login");
|
|
137
|
-
}
|
|
130
|
+
await guestRequest({
|
|
131
|
+
form: this.form,
|
|
132
|
+
url: "/login",
|
|
133
|
+
store: this.$store,
|
|
134
|
+
});
|
|
138
135
|
} catch (e) {
|
|
139
136
|
console.log(e);
|
|
140
137
|
} finally {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
<script>
|
|
51
51
|
import Form from "form-backend-validation";
|
|
52
52
|
import { ElNotification } from "element-plus";
|
|
53
|
+
import { guestRequest } from "../js/guest-request";
|
|
53
54
|
|
|
54
55
|
export default {
|
|
55
56
|
components: {
|
|
@@ -98,15 +99,11 @@ export default {
|
|
|
98
99
|
},
|
|
99
100
|
async login() {
|
|
100
101
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} catch (e) {}
|
|
107
|
-
|
|
108
|
-
await this.form.post("/login");
|
|
109
|
-
}
|
|
102
|
+
await guestRequest({
|
|
103
|
+
form: this.form,
|
|
104
|
+
url: "/login",
|
|
105
|
+
store: this.$store,
|
|
106
|
+
});
|
|
110
107
|
} catch (e) {
|
|
111
108
|
console.log(e);
|
|
112
109
|
} finally {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
<script>
|
|
49
49
|
import Form from "form-backend-validation";
|
|
50
|
+
import { guestRequest } from "../js/guest-request";
|
|
50
51
|
|
|
51
52
|
export default {
|
|
52
53
|
components: {
|
|
@@ -66,7 +67,11 @@ export default {
|
|
|
66
67
|
methods: {
|
|
67
68
|
async onSubmit() {
|
|
68
69
|
try {
|
|
69
|
-
await
|
|
70
|
+
await guestRequest({
|
|
71
|
+
form: this.form,
|
|
72
|
+
url: "/forgot-password",
|
|
73
|
+
store: this.$store,
|
|
74
|
+
});
|
|
70
75
|
|
|
71
76
|
this.$router.push({
|
|
72
77
|
name: "auth.success-forgot",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
<script>
|
|
67
67
|
import Form from "form-backend-validation";
|
|
68
|
+
import { guestRequest } from "../js/guest-request";
|
|
68
69
|
|
|
69
70
|
export default {
|
|
70
71
|
components: {
|
|
@@ -93,15 +94,11 @@ export default {
|
|
|
93
94
|
this.loading = true;
|
|
94
95
|
|
|
95
96
|
try {
|
|
96
|
-
const res = await
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} catch (e) {}
|
|
102
|
-
|
|
103
|
-
await this.form.post("/login");
|
|
104
|
-
}
|
|
97
|
+
const res = await guestRequest({
|
|
98
|
+
form: this.form,
|
|
99
|
+
url: "/login",
|
|
100
|
+
store: this.$store,
|
|
101
|
+
});
|
|
105
102
|
|
|
106
103
|
await this.postLogin();
|
|
107
104
|
} catch (e) {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<script>
|
|
59
59
|
import Form from "form-backend-validation";
|
|
60
|
+
import { guestRequest } from "../js/guest-request";
|
|
60
61
|
|
|
61
62
|
export default {
|
|
62
63
|
components: {
|
|
@@ -87,7 +88,12 @@ export default {
|
|
|
87
88
|
this.loading = true;
|
|
88
89
|
|
|
89
90
|
try {
|
|
90
|
-
const res = await
|
|
91
|
+
const res = await guestRequest({
|
|
92
|
+
form: this.form,
|
|
93
|
+
url: "/hydrate/sso/check",
|
|
94
|
+
store: this.$store,
|
|
95
|
+
});
|
|
96
|
+
|
|
91
97
|
if (res["redirect_url"]) {
|
|
92
98
|
this.redirect_url = res["redirect_url"];
|
|
93
99
|
this.setRedirect();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
|
|
2
4
|
export default {
|
|
3
5
|
metaInfo() {
|
|
4
6
|
return {
|
|
@@ -11,9 +13,15 @@ export default {
|
|
|
11
13
|
await this.$store.dispatch("logout");
|
|
12
14
|
} catch {
|
|
13
15
|
/* empty */
|
|
14
|
-
} finally {
|
|
15
|
-
this.$router.push({ name: "auth.login" });
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await axios.get("/sanctum/csrf-cookie");
|
|
20
|
+
} catch {
|
|
21
|
+
/* empty */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.$router.push({ name: "auth.login" });
|
|
17
25
|
},
|
|
18
26
|
};
|
|
19
27
|
</script>
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<script>
|
|
85
85
|
import Form from "form-backend-validation";
|
|
86
86
|
import { ElNotification } from "element-plus";
|
|
87
|
+
import { guestRequest } from "../js/guest-request";
|
|
87
88
|
|
|
88
89
|
export default {
|
|
89
90
|
components: {
|
|
@@ -116,15 +117,12 @@ export default {
|
|
|
116
117
|
this.loading = true;
|
|
117
118
|
|
|
118
119
|
try {
|
|
119
|
-
const res = await
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
} catch (e) {}
|
|
120
|
+
const res = await guestRequest({
|
|
121
|
+
form: this.form,
|
|
122
|
+
url: "/register",
|
|
123
|
+
store: this.$store,
|
|
124
|
+
});
|
|
125
125
|
|
|
126
|
-
await this.form.post("/register");
|
|
127
|
-
}
|
|
128
126
|
if (res["redirect"]) {
|
|
129
127
|
// Redirect here
|
|
130
128
|
this.$router.push({
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
<script>
|
|
68
68
|
import Form from "form-backend-validation";
|
|
69
|
+
import { guestRequest } from "../js/guest-request";
|
|
69
70
|
|
|
70
71
|
export default {
|
|
71
72
|
components: {
|
|
@@ -98,7 +99,11 @@ export default {
|
|
|
98
99
|
methods: {
|
|
99
100
|
async onSubmit() {
|
|
100
101
|
try {
|
|
101
|
-
|
|
102
|
+
await guestRequest({
|
|
103
|
+
form: this.form,
|
|
104
|
+
url: "/reset-password",
|
|
105
|
+
store: this.$store,
|
|
106
|
+
});
|
|
102
107
|
|
|
103
108
|
this.$router.push({
|
|
104
109
|
name: "auth.success-reset",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
<script>
|
|
54
54
|
import Form from "form-backend-validation";
|
|
55
55
|
import { ElNotification } from "element-plus";
|
|
56
|
+
import { guestRequest } from "../js/guest-request";
|
|
56
57
|
|
|
57
58
|
export default {
|
|
58
59
|
components: {
|
|
@@ -77,7 +78,11 @@ export default {
|
|
|
77
78
|
this.notification = null;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
await
|
|
81
|
+
await guestRequest({
|
|
82
|
+
form: this.form,
|
|
83
|
+
url: "/forgot-password",
|
|
84
|
+
store: this.$store,
|
|
85
|
+
});
|
|
81
86
|
|
|
82
87
|
ElNotification({
|
|
83
88
|
type: "success",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "element-plus";
|
|
14
14
|
import VelButton from "../../components/basic/Button.vue";
|
|
15
15
|
import VelAudit from "../../components/layout/Audit.vue";
|
|
16
|
+
import { applyTrashable } from "./trashable.js";
|
|
16
17
|
|
|
17
18
|
export const defaultResource = meta();
|
|
18
19
|
|
|
@@ -20,7 +21,7 @@ export function meta(name = "default", properties = {}) {
|
|
|
20
21
|
const singular = properties.singular || name.slice(0, -1);
|
|
21
22
|
const slug = properties.slug || kebabCase(name);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
const result = merge(
|
|
24
25
|
{
|
|
25
26
|
name,
|
|
26
27
|
title: properties.title || name[0].toUpperCase() + name.slice(1),
|
|
@@ -52,6 +53,7 @@ export function meta(name = "default", properties = {}) {
|
|
|
52
53
|
label: `Search ${name}`,
|
|
53
54
|
},
|
|
54
55
|
auditable: false,
|
|
56
|
+
trashable: false,
|
|
55
57
|
form: {
|
|
56
58
|
component: null,
|
|
57
59
|
submit: null,
|
|
@@ -393,6 +395,12 @@ export function meta(name = "default", properties = {}) {
|
|
|
393
395
|
},
|
|
394
396
|
properties,
|
|
395
397
|
);
|
|
398
|
+
|
|
399
|
+
if (result.trashable) {
|
|
400
|
+
applyTrashable(result);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return result;
|
|
396
404
|
}
|
|
397
405
|
|
|
398
406
|
export function columns(columns = []) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { h } from "vue";
|
|
3
|
+
|
|
4
|
+
import VelTableSorter from "../../components/layout/TableSorter.vue";
|
|
5
|
+
import { ElNotification } from "element-plus";
|
|
6
|
+
import VelButton from "../../components/basic/Button.vue";
|
|
7
|
+
import VelCheckbox from "../../components/form/Checkbox.vue";
|
|
8
|
+
|
|
9
|
+
export function applyTrashable(result) {
|
|
10
|
+
const originalStructure = result.index.structure;
|
|
11
|
+
result.index.structure = (props) => {
|
|
12
|
+
const structure = originalStructure(props);
|
|
13
|
+
structure["json-data"].rowClassName = ({ row }) =>
|
|
14
|
+
row.deleted_at ? "el-table__row--trashed" : "";
|
|
15
|
+
return structure;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
result.table.actions.push(({ model, resource }, props) => {
|
|
19
|
+
const { $emit } = props;
|
|
20
|
+
|
|
21
|
+
if (resource.permissions.delete(props, { model }) && model.deleted_at) {
|
|
22
|
+
return h({
|
|
23
|
+
data: () => ({
|
|
24
|
+
loading: false,
|
|
25
|
+
}),
|
|
26
|
+
render() {
|
|
27
|
+
return h(
|
|
28
|
+
VelButton,
|
|
29
|
+
{
|
|
30
|
+
tag: "a",
|
|
31
|
+
type: "success",
|
|
32
|
+
size: "small",
|
|
33
|
+
loading: this.loading,
|
|
34
|
+
onClick: async () => {
|
|
35
|
+
this.loading = true;
|
|
36
|
+
|
|
37
|
+
await axios.post(
|
|
38
|
+
`${resource.api.endpoint(props)}/${model.id}/restore`,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
$emit("reload");
|
|
42
|
+
|
|
43
|
+
ElNotification({
|
|
44
|
+
title: "Success",
|
|
45
|
+
message: `${resource.singularTitle} restored.`,
|
|
46
|
+
type: "success",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.loading = false;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
() => "Restore",
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const originalLayout = [...result.index.layout];
|
|
60
|
+
result.index.layout = [
|
|
61
|
+
(props) => {
|
|
62
|
+
const { resource } = props;
|
|
63
|
+
|
|
64
|
+
return h({
|
|
65
|
+
data: () => ({
|
|
66
|
+
showTrashed: false,
|
|
67
|
+
}),
|
|
68
|
+
methods: {
|
|
69
|
+
reload() {
|
|
70
|
+
this.$refs.table?.reload();
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
render() {
|
|
74
|
+
const structureProps = resource.index.structure(props);
|
|
75
|
+
|
|
76
|
+
if (this.showTrashed) {
|
|
77
|
+
structureProps.apiParams = {
|
|
78
|
+
...structureProps.apiParams,
|
|
79
|
+
"filter[withTrashed]": 1,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return h("div", [
|
|
84
|
+
resource.permissions.delete(props) &&
|
|
85
|
+
h(VelCheckbox, {
|
|
86
|
+
label: `Show trashed ${resource.name}`,
|
|
87
|
+
modelValue: this.showTrashed,
|
|
88
|
+
class: "mt-3",
|
|
89
|
+
"onUpdate:modelValue": (val) => {
|
|
90
|
+
this.showTrashed = val;
|
|
91
|
+
this.$nextTick(() => this.reload());
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
h(VelTableSorter, {
|
|
95
|
+
...structureProps,
|
|
96
|
+
ref: "table",
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
...originalLayout.slice(1),
|
|
103
|
+
];
|
|
104
|
+
}
|
package/components/_table.scss
CHANGED
package/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import Router from "./_Build/vue/modules/AuthModule/js/router.js";
|
|
2
2
|
import Axios from "./_Build/vue/modules/AuthModule/js/axios.js";
|
|
3
3
|
import Store from "./_Build/vue/modules/AuthModule/js/store.js";
|
|
4
|
+
import { ImpersonationPlugin } from "./_Build/vue/modules/AuthModule/js/impersonation-banner.js";
|
|
4
5
|
|
|
5
6
|
export const Auth = {
|
|
6
7
|
Router,
|
|
7
8
|
Axios,
|
|
8
9
|
Store,
|
|
10
|
+
ImpersonationPlugin,
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
export { default as Resource } from "./_Build/vue/modules/resource/index.js";
|