@fishawack/lab-velocity 2.0.0-beta.43 → 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/components/layout/TokenDisplay.vue +52 -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/PIntegrations/columns.js +58 -0
- package/_Build/vue/modules/AuthModule/routes/PIntegrations/resource.js +53 -96
- package/_Build/vue/modules/AuthModule/routes/PTeams/columns.js +78 -0
- package/_Build/vue/modules/AuthModule/routes/PTeams/resource.js +206 -290
- 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/components/_token-display.scss +41 -0
- package/general.scss +1 -0
- package/index.js +5 -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,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog
|
|
3
|
+
:model-value="true"
|
|
4
|
+
title="Token Created"
|
|
5
|
+
width="560px"
|
|
6
|
+
class="token-display"
|
|
7
|
+
:close-on-click-modal="false"
|
|
8
|
+
:close-on-press-escape="false"
|
|
9
|
+
:show-close="false"
|
|
10
|
+
>
|
|
11
|
+
<p class="token-display__warning">
|
|
12
|
+
<strong>The token below will not be shown again.</strong> Ensure
|
|
13
|
+
you've taken a copy before closing this window.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<div class="token-display__block">
|
|
17
|
+
<div class="token-display__header">
|
|
18
|
+
<span class="token-display__label">Bearer Token</span>
|
|
19
|
+
<el-button size="small" plain @click="copy">
|
|
20
|
+
{{ copied ? "Copied ✓" : "Copy" }}
|
|
21
|
+
</el-button>
|
|
22
|
+
</div>
|
|
23
|
+
<pre class="token-display__value">{{ token }}</pre>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<template #footer>
|
|
27
|
+
<el-button type="primary" @click="$emit('close')">Done</el-button>
|
|
28
|
+
</template>
|
|
29
|
+
</el-dialog>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
import { ref } from "vue";
|
|
34
|
+
import { ElDialog, ElButton } from "element-plus";
|
|
35
|
+
|
|
36
|
+
const props = defineProps({
|
|
37
|
+
token: {
|
|
38
|
+
type: String,
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
defineEmits(["close"]);
|
|
44
|
+
|
|
45
|
+
const copied = ref(false);
|
|
46
|
+
|
|
47
|
+
async function copy() {
|
|
48
|
+
await navigator.clipboard.writeText(props.token);
|
|
49
|
+
copied.value = true;
|
|
50
|
+
setTimeout(() => (copied.value = false), 2000);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
@@ -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,58 @@
|
|
|
1
|
+
import { h } from "vue";
|
|
2
|
+
|
|
3
|
+
import VelSelect from "../../../../components/form/Select.vue";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{
|
|
7
|
+
key: "name",
|
|
8
|
+
sortable: true,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: "scopes",
|
|
12
|
+
endpoint: "api/scopes",
|
|
13
|
+
labelKey: "description",
|
|
14
|
+
filterable: true,
|
|
15
|
+
clearable: true,
|
|
16
|
+
multiple: true,
|
|
17
|
+
initial: (props) =>
|
|
18
|
+
props.model?.client?.scopes?.map((id) => ({
|
|
19
|
+
id,
|
|
20
|
+
description: id,
|
|
21
|
+
})) ?? [],
|
|
22
|
+
preparation: ({ form }) => form.scopes.map((d) => d.id),
|
|
23
|
+
render: {
|
|
24
|
+
read: ({ model }) => h("span", model.client?.scopes.join(", ")),
|
|
25
|
+
write: () => h(VelSelect),
|
|
26
|
+
},
|
|
27
|
+
condition: {
|
|
28
|
+
table: false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "client_id",
|
|
33
|
+
label: "Client ID",
|
|
34
|
+
condition: {
|
|
35
|
+
form: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "user_id",
|
|
40
|
+
label: "Created by",
|
|
41
|
+
render: {
|
|
42
|
+
read: ({ model }) => h("span", model?.user?.name ?? "System"),
|
|
43
|
+
},
|
|
44
|
+
condition: {
|
|
45
|
+
form: false,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "access_logs_count",
|
|
50
|
+
label: "API Calls",
|
|
51
|
+
render: {
|
|
52
|
+
read: ({ model }) => h("span", model?.access_logs_count ?? 0),
|
|
53
|
+
},
|
|
54
|
+
condition: {
|
|
55
|
+
form: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { merge } from "lodash";
|
|
2
|
-
import {
|
|
3
|
-
import { h } from "vue";
|
|
2
|
+
import { h, ref } from "vue";
|
|
4
3
|
|
|
5
|
-
import { columns } from "../../../resource/index.js";
|
|
6
|
-
import
|
|
4
|
+
import { columns, defaultResource } from "../../../resource/index.js";
|
|
5
|
+
import integrationsColumns from "./columns.js";
|
|
6
|
+
import TokenDisplay from "../../../../components/layout/TokenDisplay.vue";
|
|
7
|
+
|
|
8
|
+
const newToken = ref(null);
|
|
7
9
|
|
|
8
10
|
export default [
|
|
9
11
|
"integrations",
|
|
@@ -23,100 +25,55 @@ export default [
|
|
|
23
25
|
edit: ({ $store }) => $store.getters.can("write integrations"),
|
|
24
26
|
delete: ({ $store }) => $store.getters.can("delete integrations"),
|
|
25
27
|
},
|
|
26
|
-
...merge(
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
condition: {
|
|
54
|
-
form: false,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
key: "user_id",
|
|
59
|
-
label: "Created by",
|
|
60
|
-
render: {
|
|
61
|
-
read: ({ model }) =>
|
|
62
|
-
h("span", model?.user?.name ?? "System"),
|
|
63
|
-
},
|
|
64
|
-
condition: {
|
|
65
|
-
form: false,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
key: "access_logs_count",
|
|
70
|
-
label: "API Calls",
|
|
71
|
-
render: {
|
|
72
|
-
read: ({ model }) =>
|
|
73
|
-
h("span", model?.access_logs_count ?? 0),
|
|
74
|
-
},
|
|
75
|
-
condition: {
|
|
76
|
-
form: false,
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
]),
|
|
80
|
-
{
|
|
81
|
-
form: {
|
|
82
|
-
submit: async (props) => {
|
|
83
|
-
const { form, resource, $router } = props;
|
|
84
|
-
const hold = JSON.parse(JSON.stringify(form.data()));
|
|
85
|
-
try {
|
|
86
|
-
form.populate(resource.form.preparation(props));
|
|
87
|
-
let res = await form.post(
|
|
88
|
-
`${resource.api.endpoint(props)}`,
|
|
89
|
-
);
|
|
90
|
-
ElMessageBox.alert(
|
|
91
|
-
`<p>The token below will not be shown again. Ensure you've taken a copy before closing this window.<br><br><strong>Token</strong>:</p><p><em>${res.data.token}</em></p>`,
|
|
92
|
-
"Token minted",
|
|
93
|
-
{
|
|
94
|
-
confirmButtonText: "Ok",
|
|
95
|
-
dangerouslyUseHTMLString: true,
|
|
96
|
-
},
|
|
97
|
-
)
|
|
98
|
-
.then(() => {
|
|
99
|
-
$router.replace({
|
|
100
|
-
name: `${resource.name}.show`,
|
|
101
|
-
params: {
|
|
102
|
-
integrationsId: res.data.id,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
})
|
|
106
|
-
.catch(() => {});
|
|
107
|
-
} catch (e) {
|
|
108
|
-
console.log(e);
|
|
109
|
-
} finally {
|
|
110
|
-
if (
|
|
111
|
-
!form.successful ||
|
|
112
|
-
!form.__options.resetOnSuccess
|
|
113
|
-
) {
|
|
114
|
-
form.populate(hold);
|
|
115
|
-
}
|
|
28
|
+
...merge(columns(integrationsColumns), {
|
|
29
|
+
form: {
|
|
30
|
+
submit: async (props) => {
|
|
31
|
+
const { form, resource, $router, model } = props;
|
|
32
|
+
const hold = JSON.parse(JSON.stringify(form.data()));
|
|
33
|
+
const isEdit = !!model;
|
|
34
|
+
const endpoint = isEdit
|
|
35
|
+
? `${resource.api.endpoint(props)}/${model.id}`
|
|
36
|
+
: `${resource.api.endpoint(props)}`;
|
|
37
|
+
try {
|
|
38
|
+
form.populate(resource.form.preparation(props));
|
|
39
|
+
let res = await form.post(endpoint);
|
|
40
|
+
newToken.value = res.data.token;
|
|
41
|
+
await $router.replace({
|
|
42
|
+
name: `${resource.name}.show`,
|
|
43
|
+
params: {
|
|
44
|
+
integrationsId: res.data.id,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.log(e);
|
|
49
|
+
} finally {
|
|
50
|
+
if (
|
|
51
|
+
!form.successful ||
|
|
52
|
+
!form.__options.resetOnSuccess
|
|
53
|
+
) {
|
|
54
|
+
form.populate(hold);
|
|
116
55
|
}
|
|
117
|
-
}
|
|
56
|
+
}
|
|
118
57
|
},
|
|
119
58
|
},
|
|
120
|
-
|
|
59
|
+
show: {
|
|
60
|
+
...defaultResource.show,
|
|
61
|
+
layout: [
|
|
62
|
+
...defaultResource.show.layout,
|
|
63
|
+
() =>
|
|
64
|
+
h({
|
|
65
|
+
setup: () => () => {
|
|
66
|
+
if (!newToken.value) return null;
|
|
67
|
+
return h(TokenDisplay, {
|
|
68
|
+
token: newToken.value,
|
|
69
|
+
onClose: () => {
|
|
70
|
+
newToken.value = null;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
121
78
|
},
|
|
122
79
|
];
|