@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.
Files changed (27) hide show
  1. package/README.md +25 -0
  2. package/_Build/vue/components/layout/PageHeader.vue +7 -6
  3. package/_Build/vue/components/layout/Table.vue +5 -0
  4. package/_Build/vue/components/layout/TableSorter.vue +1 -0
  5. package/_Build/vue/modules/AuthModule/js/guest-request.js +32 -0
  6. package/_Build/vue/modules/AuthModule/js/impersonation-banner.js +102 -0
  7. package/_Build/vue/modules/AuthModule/js/router.js +61 -27
  8. package/_Build/vue/modules/AuthModule/js/store.js +8 -0
  9. package/_Build/vue/modules/AuthModule/routes/PCompanies/resource.js +2 -3
  10. package/_Build/vue/modules/AuthModule/routes/PTeams/resource.js +1 -0
  11. package/_Build/vue/modules/AuthModule/routes/PUsers/SetPasswordAction.vue +51 -0
  12. package/_Build/vue/modules/AuthModule/routes/PUsers/SetPasswordDialog.vue +138 -0
  13. package/_Build/vue/modules/AuthModule/routes/PUsers/resource.js +39 -2
  14. package/_Build/vue/modules/AuthModule/routes/change-password.vue +6 -9
  15. package/_Build/vue/modules/AuthModule/routes/force-reset.vue +6 -9
  16. package/_Build/vue/modules/AuthModule/routes/forgot.vue +6 -1
  17. package/_Build/vue/modules/AuthModule/routes/login.vue +6 -9
  18. package/_Build/vue/modules/AuthModule/routes/loginsso.vue +7 -1
  19. package/_Build/vue/modules/AuthModule/routes/logout.vue +10 -2
  20. package/_Build/vue/modules/AuthModule/routes/register.vue +6 -8
  21. package/_Build/vue/modules/AuthModule/routes/reset.vue +6 -1
  22. package/_Build/vue/modules/AuthModule/routes/success-forgot.vue +6 -1
  23. package/_Build/vue/modules/resource/index.js +9 -1
  24. package/_Build/vue/modules/resource/trashable.js +104 -0
  25. package/components/_table.scss +3 -0
  26. package/index.js +2 -0
  27. 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="grid justify-between">
3
- <div class="grid__4/6 flex justify-start">
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 class="grid__2/6 flex gap justify-end items-start">
21
- <div class="flex gap items-center">
22
- <slot />
23
- </div>
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
 
@@ -64,6 +64,7 @@
64
64
  :label="jsonData.label"
65
65
  :over-write-id="jsonData.overWriteId"
66
66
  :fixed-height="fixedHeight"
67
+ :row-class-name="jsonData.rowClassName"
67
68
  :target-action="
68
69
  (item) => ({
69
70
  name: `${jsonData.slug}.show`,
@@ -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
- // If authenticated query param is present, assume authentication has happened elsewhere and attempt to fetch user data
184
- if (to.query.authenticated && !store.getters.authenticated) {
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
- // User verification handling and redirect if user alrady verified but accessing an expired verification route
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
- } else if (store.getters.authenticated) {
199
- // User is authenticated - check permissions and redirect appropriately
200
- if (user.force_password_change && to.name !== "auth.force-reset") {
201
- // User needs to change password - redirect to force reset
202
- next({ name: "auth.force-reset" });
203
- } else if (to.name === "auth.login") {
204
- // Already logged in user hitting login
205
- next({ name: redirect });
206
- } else if (
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
- to.matched.some((d) => d.meta.guest) !== true
228
+ !isGuestRoute &&
229
+ to.name !== "auth.verify" &&
230
+ to.name !== "auth.expired-verification"
209
231
  ) {
210
- // User needs email verification and trying to access protected route
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
- } else {
217
- // User is not authenticated - handle guest routes and login redirects
218
- if (to.matched.some((d) => d.meta.guest) === true) {
219
- // Route allows guest access - proceed
220
- next();
221
- } else {
222
- // Protected route requires authentication - redirect to standard login
223
- next({ name: "auth.login" });
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: ({ $route }) => ({
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: [
@@ -29,6 +29,7 @@ export default [
29
29
  },
30
30
  },
31
31
  auditable: true,
32
+ trashable: true,
32
33
  permissions: {
33
34
  create: ({ $store }) => $store.getters.can("write teams"),
34
35
  edit: ({ $store }) => $store.getters.can("write teams"),
@@ -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: ({ $route }) => ({
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
- const res = await this.form.post("/login");
130
-
131
- if (res["logged-in"]) {
132
- try {
133
- await this.$store.dispatch("logout");
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
- const res = await this.form.post("/login");
102
-
103
- if (res["logged-in"]) {
104
- try {
105
- await this.$store.dispatch("logout");
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 this.form.post("/forgot-password");
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 this.form.post("/login");
97
-
98
- if (res["logged-in"]) {
99
- try {
100
- await this.$store.dispatch("logout");
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 this.form.post(`/hydrate/sso/check`);
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 this.form.post("/register");
120
-
121
- if (res["logged-in"]) {
122
- try {
123
- await this.$store.dispatch("logout");
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
- const res = await this.form.post("/reset-password");
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 this.form.post("/forgot-password");
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
- return merge(
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
+ }
@@ -10,6 +10,9 @@
10
10
  .el-table__cell {
11
11
  padding: get-ratio(8px) get-ratio(12px);
12
12
  }
13
+ .el-table__row--trashed {
14
+ color: var(--el-color-danger);
15
+ }
13
16
  }
14
17
 
15
18
  .table-wrapper {
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-velocity",
3
- "version": "2.0.0-beta.44",
3
+ "version": "2.0.0-beta.45",
4
4
  "description": "Avalere Health branded style system",
5
5
  "scripts": {
6
6
  "setup": "npm ci || npm i && npm run content",