@oxygen-cms/ui 1.4.0 → 1.5.2

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 (69) hide show
  1. package/.eslintrc.js +23 -0
  2. package/.github/workflows/node.js.yml +4 -4
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/ui.iml +10 -0
  5. package/package.json +13 -5
  6. package/src/AuthApi.js +77 -42
  7. package/src/CrudApi.js +3 -3
  8. package/src/GroupsApi.js +9 -0
  9. package/src/MediaDirectoryApi.js +1 -1
  10. package/src/PreferencesApi.js +2 -0
  11. package/src/UserPermissions.js +2 -9
  12. package/src/UserPreferences.js +0 -4
  13. package/src/UserPreferences.test.js +0 -2
  14. package/src/UsersApi.js +41 -0
  15. package/src/api.js +96 -38
  16. package/src/components/App.vue +19 -240
  17. package/src/components/AuthenticatedLayout.vue +254 -0
  18. package/src/components/AuthenticationLog.vue +86 -30
  19. package/src/components/CodeEditor.vue +16 -32
  20. package/src/components/EditButtonOnRowHover.vue +21 -0
  21. package/src/components/Error404.vue +15 -5
  22. package/src/components/EventsChooser.vue +11 -11
  23. package/src/components/EventsTable.vue +14 -8
  24. package/src/components/GenericEditableField.vue +74 -0
  25. package/src/components/GroupsChooser.vue +58 -0
  26. package/src/components/GroupsList.vue +129 -0
  27. package/src/components/ImportExport.vue +32 -1
  28. package/src/components/LegacyPage.vue +22 -23
  29. package/src/components/UserJoined.vue +35 -0
  30. package/src/components/UserManagement.vue +168 -0
  31. package/src/components/UserProfileForm.vue +214 -0
  32. package/src/components/ViewProfile.vue +7 -219
  33. package/src/components/auth/Auth404.vue +16 -0
  34. package/src/components/auth/Login.vue +135 -0
  35. package/src/components/auth/LoginLogo.vue +30 -0
  36. package/src/components/auth/Logout.vue +26 -0
  37. package/src/components/auth/PasswordRemind.vue +71 -0
  38. package/src/components/auth/PasswordReset.vue +97 -0
  39. package/src/components/auth/TwoFactorSetup.vue +115 -0
  40. package/src/components/auth/VerifyEmail.vue +71 -0
  41. package/src/components/auth/WelcomeFloat.vue +87 -0
  42. package/src/components/auth/login.scss +17 -0
  43. package/src/components/{MediaChooseDirectory.vue → media/MediaChooseDirectory.vue} +12 -12
  44. package/src/components/{MediaDirectory.vue → media/MediaDirectory.vue} +8 -8
  45. package/src/components/{MediaInsertModal.vue → media/MediaInsertModal.vue} +2 -2
  46. package/src/components/{MediaItem.vue → media/MediaItem.vue} +24 -23
  47. package/src/components/{MediaItemPreview.vue → media/MediaItemPreview.vue} +5 -5
  48. package/src/components/{MediaList.vue → media/MediaList.vue} +42 -38
  49. package/src/components/{MediaPage.vue → media/MediaPage.vue} +1 -1
  50. package/src/components/{MediaResponsiveImages.vue → media/MediaResponsiveImages.vue} +5 -5
  51. package/src/components/{MediaUpload.vue → media/MediaUpload.vue} +10 -10
  52. package/src/components/{media.scss → media/media.scss} +1 -1
  53. package/src/components/preferences/PreferencesField.vue +10 -10
  54. package/src/components/preferences/PreferencesList.vue +13 -20
  55. package/src/components/preferences/PreferencesThemeChooser.vue +9 -9
  56. package/src/components/preferences/ShowIfPermitted.vue +9 -14
  57. package/src/components/users/CreateUserModal.vue +73 -0
  58. package/src/icons.js +90 -0
  59. package/src/main.js +111 -0
  60. package/src/modules/LegacyPages.js +18 -0
  61. package/src/modules/Media.js +45 -0
  62. package/src/modules/UserManagement.js +24 -0
  63. package/src/routes/index.js +92 -0
  64. package/src/store/index.js +70 -0
  65. package/src/styles/_variables.scss +1 -0
  66. package/src/styles/app.scss +15 -2
  67. package/src/login.js +0 -17
  68. package/src/routes.js +0 -61
  69. package/src/styles/login.scss +0 -86
package/.eslintrc.js ADDED
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ "env": {
3
+ "browser": true,
4
+ "es2021": true,
5
+ "node": true,
6
+ "jest/globals": true
7
+ },
8
+ "extends": [
9
+ "eslint:recommended",
10
+ "plugin:vue/recommended",
11
+ "prettier"
12
+ ],
13
+ "parserOptions": {
14
+ "ecmaVersion": 12,
15
+ "sourceType": "module"
16
+ },
17
+ "plugins": [
18
+ "vue",
19
+ "jest"
20
+ ],
21
+ "rules": {
22
+ }
23
+ };
@@ -16,7 +16,7 @@ jobs:
16
16
 
17
17
  strategy:
18
18
  matrix:
19
- node-version: [10.x, 12.x, 14.x]
19
+ node-version: [16.x]
20
20
 
21
21
  steps:
22
22
  - uses: actions/checkout@v2
@@ -24,6 +24,6 @@ jobs:
24
24
  uses: actions/setup-node@v1
25
25
  with:
26
26
  node-version: ${{ matrix.node-version }}
27
- - run: npm install -g yarn
28
- - run: yarn install
29
- - run: yarn test
27
+ - run: npm ci
28
+ - run: npm run lint
29
+ - run: npm run test
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/ui.iml" filepath="$PROJECT_DIR$/.idea/ui.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
package/.idea/ui.iml ADDED
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
6
+ </content>
7
+ <orderEntry type="inheritedJdk" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ </module>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-cms/ui",
3
- "version": "1.4.0",
3
+ "version": "1.5.2",
4
4
  "description": "Various utilities for UI-building in Vue.js",
5
5
  "main": "none",
6
6
  "repository": {
@@ -18,9 +18,10 @@
18
18
  "autoprefixer": "^9.8.5",
19
19
  "babel-loader": "^8.1.0",
20
20
  "brace": "^0.11.1",
21
- "buefy": "^0.9.4",
22
- "bulma": "~0.9.1",
21
+ "buefy": "^0.9.10",
22
+ "bulma": "~0.9.3",
23
23
  "copy-webpack-plugin": "^5.1.1",
24
+ "downloadjs": "^1.4.7",
24
25
  "file-loader": "^6.1.1",
25
26
  "libphonenumber-js": "^1.9.11",
26
27
  "lodash": "^4.17.21",
@@ -29,10 +30,12 @@
29
30
  "v-hotkey": "^0.8.0",
30
31
  "vex-js": "~4.1.0",
31
32
  "vue": "^2.6.11",
33
+ "vue-async-computed": "^3.9.0",
32
34
  "vue-loader": "^15.9.6",
33
35
  "vue-router": "^3.5.1",
34
36
  "vue-template-compiler": "^2.6.11",
35
- "vue2-ace-editor": "^0.0.15"
37
+ "vue2-ace-editor": "^0.0.15",
38
+ "vuex": "^3.6.2"
36
39
  },
37
40
  "devDependencies": {
38
41
  "@babel/core": "^7.13.1",
@@ -40,6 +43,10 @@
40
43
  "babel-jest": "^26.6.3",
41
44
  "babel-polyfill": "^6.26.0",
42
45
  "css-loader": "^3.5.1",
46
+ "eslint": "^7.32.0",
47
+ "eslint-config-prettier": "^8.3.0",
48
+ "eslint-plugin-jest": "^24.4.0",
49
+ "eslint-plugin-vue": "^7.17.0",
43
50
  "jest": "^26.6.3",
44
51
  "node-sass": "^4.13.1",
45
52
  "postcss-loader": "^3.0.0",
@@ -49,7 +56,8 @@
49
56
  "webpack-cli": "^3.3.11"
50
57
  },
51
58
  "scripts": {
52
- "test": "jest"
59
+ "test": "jest",
60
+ "lint": "eslint --ext js,vue src/"
53
61
  },
54
62
  "jest": {
55
63
  "verbose": true,
package/src/AuthApi.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {API_ROOT} from "./CrudApi";
2
- import {FetchBuilder} from "./api";
2
+ import {FetchBuilder, initCsrfCookie} from "./api";
3
+ import UserPermissions from "./UserPermissions";
3
4
 
4
5
  export default class AuthApi {
5
6
 
@@ -11,41 +12,62 @@ export default class AuthApi {
11
12
  return FetchBuilder.default(this.$buefy, method);
12
13
  }
13
14
 
14
- async logout() {
15
- let data = await this.request('post')
16
- .fetch(API_ROOT + 'auth/logout');
17
- window.location = data.redirect;
18
- return data;
15
+ async login(username, password, code) {
16
+ return await this.request('post')
17
+ .withJson({
18
+ username,
19
+ password,
20
+ '2fa_code': code
21
+ })
22
+ .fetch(API_ROOT + 'auth/login');
19
23
  }
20
24
 
21
- async stopImpersonating() {
22
- let data = await this.request('post')
23
- .fetch('/oxygen/view/users/leaveImpersonate');
24
- console.log(data);
25
- window.location = data.redirect;
26
- return data;
25
+ async getLoginPreferences() {
26
+ return await this.request('get').fetch(API_ROOT + 'auth/preferences');
27
27
  }
28
28
 
29
- async userDetails() {
30
- if(AuthApi.currentUserDetails) {
31
- return AuthApi.currentUserDetails;
32
- } else if(AuthApi.currentlyFetchingUserDetails === true) {
33
- return new Promise((resolve, reject) => {
34
- AuthApi.userDetailsResolvedHooks.push(resolve);
35
- });
36
- }
29
+ async setupTwoFactorAuth() {
30
+ return await this.request('post')
31
+ .fetch(API_ROOT + 'auth/two-factor-setup');
32
+ }
37
33
 
38
- AuthApi.currentlyFetchingUserDetails = true;
39
- let response = await this.request('get')
40
- .fetch(API_ROOT + 'auth/user');
34
+ async confirmTwoFactorAuth(code) {
35
+ return await this.request('post')
36
+ .withJson({
37
+ '2fa_code': code
38
+ })
39
+ .fetch(API_ROOT + 'auth/two-factor-confirm');
40
+ }
41
41
 
42
- AuthApi.currentUserDetails = response;
43
- AuthApi.currentlyFetchingUserDetails = false;
44
- for(let hook of AuthApi.userDetailsResolvedHooks) {
45
- hook(response);
46
- }
47
- AuthApi.userDetailsResolvedHooks = [];
42
+ async sendReminderEmail(email) {
43
+ return await this.request('post')
44
+ .withJson({
45
+ 'email': email
46
+ })
47
+ .fetch(API_ROOT + 'auth/send-reminder-email');
48
+ }
48
49
 
50
+ async sendEmailVerification() {
51
+ return await this.request('post')
52
+ .fetch(API_ROOT + 'auth/verify-email');
53
+ }
54
+
55
+ async resetPassword(params) {
56
+ return await this.request('post')
57
+ .withJson(params)
58
+ .fetch(API_ROOT + 'auth/reset-password');
59
+ }
60
+
61
+ async logout() {
62
+ let response = await this.request('post')
63
+ .fetch(API_ROOT + 'auth/logout');
64
+ this.$buefy.notification.open({
65
+ message: 'You have been logged out',
66
+ type: 'is-info',
67
+ duration: 4000,
68
+ queue: false
69
+ });
70
+ await initCsrfCookie();
49
71
  return response;
50
72
  }
51
73
 
@@ -61,21 +83,34 @@ export default class AuthApi {
61
83
  .fetch(API_ROOT + 'auth/change-password');
62
84
  }
63
85
 
64
- async updateFullName(name) {
65
- return this.request('put')
66
- .withJson({
67
- fullName: name
68
- })
69
- .fetch(API_ROOT + 'auth/fullName');
86
+ async listUserSessions() {
87
+ return this.request('get')
88
+ .fetch(API_ROOT + 'auth/sessions')
70
89
  }
71
90
 
72
- async terminateAccount() {
73
- return this.request('post')
74
- .withJson(params)
75
- .fetch(API_ROOT + 'auth/terminate-account');
91
+ async deleteUserSession(id) {
92
+ return this.request('delete')
93
+ .fetch(API_ROOT + 'auth/sessions/' + id)
76
94
  }
77
95
  }
78
96
 
79
- AuthApi.currentlyFetchingUserDetails = false;
80
- AuthApi.currentUserDetails = null;
81
- AuthApi.userDetailsResolvedHooks = [];
97
+ export const checkAuthenticated = (store) => {
98
+ return (to, from, next) => {
99
+ store.dispatch('determineLoginStatus').then((isLoggedIn) => {
100
+ if(!to.path.startsWith('/auth/login') && !isLoggedIn && to.meta.allowUnauthenticated !== true) {
101
+ UserPermissions.$buefy.notification.open({
102
+ message: 'You need to be logged in to view that page',
103
+ type: 'is-info',
104
+ queue: false,
105
+ duration: 7000
106
+ });
107
+ next({
108
+ path: '/auth/login',
109
+ query: { redirect: to.fullPath }
110
+ });
111
+ } else {
112
+ next();
113
+ }
114
+ })
115
+ }
116
+ };
package/src/CrudApi.js CHANGED
@@ -66,20 +66,20 @@ class CrudApi {
66
66
  .fetch(this.constructor.getResourceRoot() + '/search');
67
67
  }
68
68
 
69
- async forceDelete(id, callback) {
69
+ async forceDelete(id) {
70
70
  return this.request('delete')
71
71
  .fetch(this.constructor.getResourceRoot() + '/' + id + '?force=true');
72
72
  }
73
73
 
74
74
  async confirmForceDelete(id) {
75
- const promise = new Promise((resolve, reject) => {
75
+ const promise = new Promise((resolve) => {
76
76
  this.$buefy.dialog.confirm({
77
77
  message: 'Are you sure you want to delete this record forever?',
78
78
  onConfirm: resolve
79
79
  });
80
80
  });
81
81
 
82
- let result = await promise;
82
+ await promise;
83
83
  let data = await this.forceDelete(id);
84
84
  this.$buefy.toast.open(morphToNotification(data));
85
85
  return data;
@@ -0,0 +1,9 @@
1
+ import {CrudApi} from './CrudApi';
2
+
3
+ export default class GroupsApi extends CrudApi {
4
+
5
+ static getResourceName() {
6
+ return 'groups';
7
+ }
8
+
9
+ }
@@ -3,7 +3,7 @@ import { CrudApi } from './CrudApi';
3
3
  export default class MediaDirectoryApi extends CrudApi {
4
4
 
5
5
  static getResourceName() {
6
- return 'mediaDirectory';
6
+ return 'media-directory';
7
7
  }
8
8
 
9
9
  static prepareModelForAPI(data) {
@@ -6,9 +6,11 @@ export const canAccessPrefs = ($buefy, userPermissions, keys) => {
6
6
  for(let key of keys) {
7
7
  let permissionsKey = 'preferences.' + key.split('::')[0].replace('.', '_');
8
8
  if(userPermissions.has(permissionsKey)) {
9
+ console.log('Granted');
9
10
  return true;
10
11
  }
11
12
  }
13
+ console.log('Denied');
12
14
  return false;
13
15
  };
14
16
 
@@ -1,4 +1,4 @@
1
- // this is a straight JavaScript port of the `Oxygen\Auth\Permissions\SimplePermissionsSystem` PHP class.
1
+ // this is a straight JavaScript port of the `Oxygen\Auth\Permissions\TreePermissionsSystem` PHP class.
2
2
 
3
3
  export default class UserPermissions {
4
4
 
@@ -7,7 +7,6 @@ export default class UserPermissions {
7
7
  }
8
8
 
9
9
  static get ROOT_CONTENT_TYPE() { return '_root'; }
10
- static get ACCESS_KEY() { return '_access'; }
11
10
  static get PARENT_KEY() { return '_parent'; }
12
11
  static get MAX_INHERITANCE_DEPTH() { return 10; }
13
12
 
@@ -24,13 +23,7 @@ export default class UserPermissions {
24
23
  let keyParts = key.split('.');
25
24
 
26
25
  if(keyParts.length !== 2) {
27
- throw new Error('SimplePermissionsSystem Requires a Dot-Seperated Permissions Key');
28
- }
29
-
30
- // check for the access key
31
- if(!this.hasKey(keyParts[0], UserPermissions.ACCESS_KEY)) {
32
- console.warn('no access');
33
- return false;
26
+ throw new Error('TreePermissionsSystem Requires a Dot-Seperated Permissions Key');
34
27
  }
35
28
 
36
29
  // check for the specific key
@@ -19,10 +19,6 @@ class UserPreferences {
19
19
  this.authApi = new AuthApi(this.$buefy);
20
20
  }
21
21
 
22
- static async load() {
23
- return new UserPreferences((await this.authApi.userDetails()).user.preferences);
24
- }
25
-
26
22
  get(key, fallback = null) {
27
23
  let o = this.preferences;
28
24
 
@@ -1,9 +1,7 @@
1
1
  import UserPreferences from './UserPreferences';
2
- import AuthApi from "./AuthApi";
3
2
 
4
3
  jest.mock('./AuthApi');
5
4
 
6
-
7
5
  test('gets and sets preferences', async () => {
8
6
  UserPreferences.setBuefy({});
9
7
  UserPreferences.authApi.userDetails.mockResolvedValue({
@@ -0,0 +1,41 @@
1
+ import {API_ROOT, CrudApi} from './CrudApi';
2
+
3
+ export default class UsersApi extends CrudApi {
4
+
5
+ static prepareModelForAPI(data) {
6
+ let m = { ...data };
7
+ delete m.id;
8
+ if(m.group) {
9
+ m.group = m.group.id;
10
+ }
11
+ return m;
12
+ }
13
+
14
+ static getResourceName() {
15
+ return 'users';
16
+ }
17
+
18
+ async updateFullName(id, name) {
19
+ return this.request('put')
20
+ .withJson({
21
+ fullName: name
22
+ })
23
+ .fetch(API_ROOT + 'users/' + id + '/fullName');
24
+ }
25
+
26
+ async impersonate(id) {
27
+ return await this.request('post')
28
+ .fetch(API_ROOT + 'users/' + id + '/impersonate');
29
+ }
30
+
31
+ async forceDelete(id) {
32
+ return this.request('delete')
33
+ .fetch(this.constructor.getResourceRoot() + '/' + id + '/force');
34
+ }
35
+
36
+ async stopImpersonating() {
37
+ return await this.request('post')
38
+ .fetch(API_ROOT + 'users/stop-impersonating');
39
+ }
40
+
41
+ }
package/src/api.js CHANGED
@@ -1,12 +1,21 @@
1
- const getCSRFToken = () => {
2
- let csrfTokenNode = document.querySelector('meta[name="csrf-token"]');
3
- if(csrfTokenNode === null) {
4
- return null;
5
- }
6
- return csrfTokenNode.attributes.content.nodeValue;
7
- };
1
+ var xsrfToken = null;
2
+
3
+ export const initCsrfCookie = async () => {
4
+ await window.fetch(
5
+ '/sanctum/csrf-cookie',
6
+ {
7
+ credentials: 'same-origin'
8
+ });
9
+ let cookiesObj = document.cookie
10
+ .split(';')
11
+ .reduce((res, c) => {
12
+ const [key, val] = c.trim().split('=').map(decodeURIComponent)
13
+ return Object.assign(res, { [key]: val });
14
+ }, {});
15
+ xsrfToken = cookiesObj['XSRF-TOKEN'];
16
+ }
8
17
 
9
- class FetchBuilder {
18
+ export class FetchBuilder {
10
19
  constructor($buefy, method) {
11
20
  this.$buefy = $buefy;
12
21
  this.method = method;
@@ -36,32 +45,40 @@ class FetchBuilder {
36
45
  return this;
37
46
  }
38
47
 
39
- withCsrfToken() {
40
- this.headers.set('X-CSRF-TOKEN', getCSRFToken());
41
- return this;
42
- }
43
-
44
48
  cookies() {
45
49
  this.credentials = 'same-origin';
46
50
  return this;
47
51
  }
48
52
 
49
- async fetch(url) {
53
+ async setXsrfTokenHeader() {
54
+ if(xsrfToken === null) {
55
+ await initCsrfCookie();
56
+ }
57
+ this.headers.set('X-XSRF-TOKEN', xsrfToken);
58
+ }
59
+
60
+ async fetchRaw(url) {
61
+ await this.setXsrfTokenHeader()
62
+
50
63
  let v = { ... this};
51
64
  v.queryParams = undefined;
52
65
 
53
66
  if(this.queryParams) {
54
67
  url = new URL(url, window.location);
55
68
  for(let name in this.queryParams) {
56
- if(this.queryParams.hasOwnProperty(name) && this.queryParams[name] !== null) {
69
+ if(Object.prototype.hasOwnProperty.call(this.queryParams, name) && this.queryParams[name] !== null) {
57
70
  url.searchParams.append(name, this.queryParams[name]);
58
71
  }
59
72
  }
60
73
  }
61
74
 
62
- let response = await window.fetch(url.toString(), this);
75
+ return await window.fetch(url.toString(), this);
76
+ }
77
+
78
+ async fetch(url) {
79
+ let response = await this.fetchRaw(url);
63
80
 
64
- let data;
81
+ let data = {};
65
82
  try {
66
83
  data = await response.json();
67
84
  } catch(e) {
@@ -73,23 +90,25 @@ class FetchBuilder {
73
90
  queue: false
74
91
  });
75
92
  return {};
76
- }
77
-
78
- console.error('Response did not contain valid JSON: ', e);
79
- this.$buefy.notification.open({
80
- message: 'Whoops, looks like something went wrong.',
81
- type: 'is-warning',
82
- queue: false
83
- });
93
+ } else if(response.status === 204) {
94
+ // no content, we're okay
95
+ } else {
96
+ console.error('Response did not contain valid JSON: ', e);
97
+ this.$buefy.notification.open({
98
+ message: 'Whoops, looks like something went wrong.',
99
+ type: 'is-warning',
100
+ queue: false
101
+ });
84
102
 
85
- throw e;
103
+ throw e;
104
+ }
86
105
  }
87
106
 
88
107
  if(response.ok && data.status !== 'failed') {
89
108
  return data;
90
109
  }
91
110
 
92
- handleAPIError(data, this.$buefy);
111
+ handleAPIError(data, this.$buefy, FetchBuilder.router, response);
93
112
  let e = new Error('Received an error response from API call');
94
113
  e.response = data;
95
114
  throw e;
@@ -98,9 +117,12 @@ class FetchBuilder {
98
117
  static default($buefy, method) {
99
118
  return (new FetchBuilder($buefy, method))
100
119
  .cookies()
101
- .withCsrfToken()
102
120
  .wantJson();
103
121
  }
122
+
123
+ static setRouter(router) {
124
+ FetchBuilder.router = router;
125
+ }
104
126
  }
105
127
 
106
128
  function statusToBueify(status) {
@@ -111,7 +133,7 @@ function statusToBueify(status) {
111
133
  }
112
134
  }
113
135
 
114
- function morphToNotification(data) {
136
+ export function morphToNotification(data) {
115
137
  return {
116
138
  message: data.content,
117
139
  type: statusToBueify(data.status),
@@ -121,28 +143,60 @@ function morphToNotification(data) {
121
143
  };
122
144
  }
123
145
 
124
- const handleAPIError = function(content, $buefy) {
146
+ const handleAPIError = function(content, $buefy, $router, response) {
125
147
  console.error('API error: ', content);
126
- if(content.authenticated === false) {
148
+ if(response.status === 401 && content.code === 'unauthenticated') {
127
149
  // server is telling us to login again
128
- window.location.replace('/oxygen/auth/login?intended=' + window.location);
150
+ initCsrfCookie()
151
+ .then(() => {
152
+ $router.push({path: '/auth/login', query: {redirect: $router.currentRoute.fullPath}});
153
+ });
154
+ return;
155
+ } else if(response.status === 403 && content.code === 'two_factor_setup_required') {
156
+ $router.push({ path: '/auth/2fa-setup' });
157
+ return;
158
+ } else if(response.status === 403 && content.code === 'email_unverified') {
159
+ $router.push({ path: '/auth/needs-verified-email', query: {redirect: $router.currentRoute.fullPath } });
160
+ return;
161
+ } else if(response.status === 429) {
162
+ $buefy.notification.open({
163
+ message: 'Too many requests within a short timeframe. Please wait.',
164
+ type: 'is-warning',
165
+ duration: 10000,
166
+ queue: false
167
+ });
168
+ return;
169
+ }
170
+
171
+ // handle generic validation errors
172
+ if(typeof content.errors === 'object') {
173
+ for(const [, errors ] of Object.entries(content.errors)) {
174
+ for(let error of errors) {
175
+ $buefy.notification.open({
176
+ message: error,
177
+ duration: 4000,
178
+ queue: false,
179
+ type: 'is-warning'
180
+ });
181
+ }
182
+ }
129
183
  return;
130
184
  }
131
185
 
132
186
  if(content.content && content.status) {
133
187
  $buefy.notification.open(morphToNotification(content));
134
- } else if(content.error) {
188
+ } else if(content.exception) {
135
189
  $buefy.notification.open({
136
190
  message:
137
- 'PHP Exception of type <pre class="no-pre">' + content.error.type +
138
- '</pre> with message <pre class="no-pre">' + content.error.message +
139
- '</pre> thrown at <pre class="no-pre">' + content.error.file + ':' + content.error.line +
191
+ 'PHP Exception of type <pre class="no-pre">' + content.exception +
192
+ '</pre> with message <pre class="no-pre">' + content.message +
193
+ '</pre> thrown at <pre class="no-pre">' + content.file + ':' + content.line +
140
194
  '</pre>',
141
195
  duration: 20000,
142
196
  animation: 'fade',
143
197
  type: 'is-danger'
144
198
  });
145
- } else {
199
+ } else if(response.status === 500) {
146
200
  $buefy.notification.open({
147
201
  message:'Whoops, looks like something went wrong.',
148
202
  type: 'is-danger',
@@ -152,4 +206,8 @@ const handleAPIError = function(content, $buefy) {
152
206
  }
153
207
  };
154
208
 
155
- export { FetchBuilder, morphToNotification, getCSRFToken };
209
+ export function getXsrfToken() {
210
+ return xsrfToken;
211
+ }
212
+
213
+ FetchBuilder.router = null;