@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.
- package/.eslintrc.js +23 -0
- package/.github/workflows/node.js.yml +4 -4
- package/.idea/modules.xml +8 -0
- package/.idea/ui.iml +10 -0
- package/package.json +13 -5
- package/src/AuthApi.js +77 -42
- package/src/CrudApi.js +3 -3
- package/src/GroupsApi.js +9 -0
- package/src/MediaDirectoryApi.js +1 -1
- package/src/PreferencesApi.js +2 -0
- package/src/UserPermissions.js +2 -9
- package/src/UserPreferences.js +0 -4
- package/src/UserPreferences.test.js +0 -2
- package/src/UsersApi.js +41 -0
- package/src/api.js +96 -38
- package/src/components/App.vue +19 -240
- package/src/components/AuthenticatedLayout.vue +254 -0
- package/src/components/AuthenticationLog.vue +86 -30
- package/src/components/CodeEditor.vue +16 -32
- package/src/components/EditButtonOnRowHover.vue +21 -0
- package/src/components/Error404.vue +15 -5
- package/src/components/EventsChooser.vue +11 -11
- package/src/components/EventsTable.vue +14 -8
- package/src/components/GenericEditableField.vue +74 -0
- package/src/components/GroupsChooser.vue +58 -0
- package/src/components/GroupsList.vue +129 -0
- package/src/components/ImportExport.vue +32 -1
- package/src/components/LegacyPage.vue +22 -23
- package/src/components/UserJoined.vue +35 -0
- package/src/components/UserManagement.vue +168 -0
- package/src/components/UserProfileForm.vue +214 -0
- package/src/components/ViewProfile.vue +7 -219
- package/src/components/auth/Auth404.vue +16 -0
- package/src/components/auth/Login.vue +135 -0
- package/src/components/auth/LoginLogo.vue +30 -0
- package/src/components/auth/Logout.vue +26 -0
- package/src/components/auth/PasswordRemind.vue +71 -0
- package/src/components/auth/PasswordReset.vue +97 -0
- package/src/components/auth/TwoFactorSetup.vue +115 -0
- package/src/components/auth/VerifyEmail.vue +71 -0
- package/src/components/auth/WelcomeFloat.vue +87 -0
- package/src/components/auth/login.scss +17 -0
- package/src/components/{MediaChooseDirectory.vue → media/MediaChooseDirectory.vue} +12 -12
- package/src/components/{MediaDirectory.vue → media/MediaDirectory.vue} +8 -8
- package/src/components/{MediaInsertModal.vue → media/MediaInsertModal.vue} +2 -2
- package/src/components/{MediaItem.vue → media/MediaItem.vue} +24 -23
- package/src/components/{MediaItemPreview.vue → media/MediaItemPreview.vue} +5 -5
- package/src/components/{MediaList.vue → media/MediaList.vue} +42 -38
- package/src/components/{MediaPage.vue → media/MediaPage.vue} +1 -1
- package/src/components/{MediaResponsiveImages.vue → media/MediaResponsiveImages.vue} +5 -5
- package/src/components/{MediaUpload.vue → media/MediaUpload.vue} +10 -10
- package/src/components/{media.scss → media/media.scss} +1 -1
- package/src/components/preferences/PreferencesField.vue +10 -10
- package/src/components/preferences/PreferencesList.vue +13 -20
- package/src/components/preferences/PreferencesThemeChooser.vue +9 -9
- package/src/components/preferences/ShowIfPermitted.vue +9 -14
- package/src/components/users/CreateUserModal.vue +73 -0
- package/src/icons.js +90 -0
- package/src/main.js +111 -0
- package/src/modules/LegacyPages.js +18 -0
- package/src/modules/Media.js +45 -0
- package/src/modules/UserManagement.js +24 -0
- package/src/routes/index.js +92 -0
- package/src/store/index.js +70 -0
- package/src/styles/_variables.scss +1 -0
- package/src/styles/app.scss +15 -2
- package/src/login.js +0 -17
- package/src/routes.js +0 -61
- 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: [
|
|
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
|
|
28
|
-
- run:
|
|
29
|
-
- run:
|
|
27
|
+
- run: npm ci
|
|
28
|
+
- run: npm run lint
|
|
29
|
+
- run: npm run test
|
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.
|
|
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.
|
|
22
|
-
"bulma": "~0.9.
|
|
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
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
22
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
65
|
-
return this.request('
|
|
66
|
-
.
|
|
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
|
|
73
|
-
return this.request('
|
|
74
|
-
.
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
82
|
+
await promise;
|
|
83
83
|
let data = await this.forceDelete(id);
|
|
84
84
|
this.$buefy.toast.open(morphToNotification(data));
|
|
85
85
|
return data;
|
package/src/GroupsApi.js
ADDED
package/src/MediaDirectoryApi.js
CHANGED
package/src/PreferencesApi.js
CHANGED
|
@@ -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
|
|
package/src/UserPermissions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// this is a straight JavaScript port of the `Oxygen\Auth\Permissions\
|
|
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('
|
|
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
|
package/src/UserPreferences.js
CHANGED
|
@@ -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
|
|
package/src/UsersApi.js
ADDED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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.
|
|
148
|
+
if(response.status === 401 && content.code === 'unauthenticated') {
|
|
127
149
|
// server is telling us to login again
|
|
128
|
-
|
|
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.
|
|
188
|
+
} else if(content.exception) {
|
|
135
189
|
$buefy.notification.open({
|
|
136
190
|
message:
|
|
137
|
-
'PHP Exception of type <pre class="no-pre">' + content.
|
|
138
|
-
'</pre> with message <pre class="no-pre">' + content.
|
|
139
|
-
'</pre> thrown at <pre class="no-pre">' + content.
|
|
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
|
|
209
|
+
export function getXsrfToken() {
|
|
210
|
+
return xsrfToken;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
FetchBuilder.router = null;
|