From 28a588d8c5b28ebf1f6a93d0fa6869a42fa06b16 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 26 Mar 2024 16:04:08 +0100 Subject: [PATCH 1/2] AIMAAS #197: refactor users/groups state management --- frontend/src/components/Entity.vue | 34 +- frontend/src/components/EntityList.vue | 6 +- frontend/src/components/Schema.vue | 26 +- frontend/src/components/auth/AuthManager.vue | 65 +- frontend/src/components/auth/GroupEdit.vue | 79 -- frontend/src/components/auth/GroupForm.vue | 66 ++ .../src/components/auth/GroupListItem.vue | 15 +- frontend/src/components/auth/GroupManager.vue | 193 +++-- frontend/src/components/auth/GroupMembers.vue | 288 +++---- .../src/components/auth/GroupMemberships.vue | 23 + .../src/components/auth/PermissionList.vue | 790 ++++++++++-------- frontend/src/components/auth/UserDetails.vue | 49 +- frontend/src/components/auth/UserManager.vue | 58 +- frontend/src/components/inputs/EntityForm.vue | 2 +- frontend/src/components/layout/Tabbing.vue | 33 +- frontend/src/composables/alert.js | 44 + frontend/src/composables/api.js | 524 ++++++++++++ frontend/src/composables/auth.js | 25 - frontend/src/plugins/alert.js | 47 +- frontend/src/plugins/api.js | 493 +---------- frontend/src/store/auth.js | 96 +++ 21 files changed, 1564 insertions(+), 1392 deletions(-) delete mode 100644 frontend/src/components/auth/GroupEdit.vue create mode 100644 frontend/src/components/auth/GroupForm.vue create mode 100644 frontend/src/components/auth/GroupMemberships.vue create mode 100644 frontend/src/composables/alert.js create mode 100644 frontend/src/composables/api.js delete mode 100644 frontend/src/composables/auth.js create mode 100644 frontend/src/store/auth.js diff --git a/frontend/src/components/Entity.vue b/frontend/src/components/Entity.vue index da66ab3..4eb3fa3 100644 --- a/frontend/src/components/Entity.vue +++ b/frontend/src/components/Entity.vue @@ -15,12 +15,12 @@ - - - \ No newline at end of file diff --git a/frontend/src/components/auth/GroupEdit.vue b/frontend/src/components/auth/GroupEdit.vue deleted file mode 100644 index b0d5623..0000000 --- a/frontend/src/components/auth/GroupEdit.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/components/auth/GroupForm.vue b/frontend/src/components/auth/GroupForm.vue new file mode 100644 index 0000000..b662104 --- /dev/null +++ b/frontend/src/components/auth/GroupForm.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/src/components/auth/GroupListItem.vue b/frontend/src/components/auth/GroupListItem.vue index 7302a12..6554c67 100644 --- a/frontend/src/components/auth/GroupListItem.vue +++ b/frontend/src/components/auth/GroupListItem.vue @@ -29,12 +29,19 @@ export default { name: "GroupListItem", emits: ["groupSelected"], - inject: ["groups", "tree"], props: { group: { required: true, type: Object, - } + }, + groups: { + required: true, + type: Object, + }, + tree: { + required: true, + type: Object, + }, }, computed: { collapseId() { @@ -54,7 +61,3 @@ export default { } } - - \ No newline at end of file diff --git a/frontend/src/components/auth/GroupManager.vue b/frontend/src/components/auth/GroupManager.vue index 5f679a7..41f4d1e 100644 --- a/frontend/src/components/auth/GroupManager.vue +++ b/frontend/src/components/auth/GroupManager.vue @@ -1,116 +1,133 @@ - - \ No newline at end of file +.group-detail { + flex: 2; + min-width: 360px; +} + diff --git a/frontend/src/components/auth/GroupMembers.vue b/frontend/src/components/auth/GroupMembers.vue index 23a9779..3b3b833 100644 --- a/frontend/src/components/auth/GroupMembers.vue +++ b/frontend/src/components/auth/GroupMembers.vue @@ -1,156 +1,162 @@ - - \ No newline at end of file +await Promise.all([ loadUserData(), getMembers() ]) + diff --git a/frontend/src/components/auth/GroupMemberships.vue b/frontend/src/components/auth/GroupMemberships.vue new file mode 100644 index 0000000..25ae82e --- /dev/null +++ b/frontend/src/components/auth/GroupMemberships.vue @@ -0,0 +1,23 @@ + + diff --git a/frontend/src/components/auth/PermissionList.vue b/frontend/src/components/auth/PermissionList.vue index 715f9c3..75f2367 100644 --- a/frontend/src/components/auth/PermissionList.vue +++ b/frontend/src/components/auth/PermissionList.vue @@ -1,387 +1,445 @@ - - \ No newline at end of file +async function onDelete() { + await api.revokePermissions({ + permissionIds: selected.value.map((x) => parseInt(x)), + }); + await load(); +} + +async function onAddition() { + await api.grantPermission(newPermData.value); + await load(); +} + +await Promise.all([loadGroupData(), loadUserData()]); + diff --git a/frontend/src/components/auth/UserDetails.vue b/frontend/src/components/auth/UserDetails.vue index e0edbbb..b2fac37 100644 --- a/frontend/src/components/auth/UserDetails.vue +++ b/frontend/src/components/auth/UserDetails.vue @@ -30,62 +30,41 @@
Group memberships
- - - \ No newline at end of file +.user-detail { + flex: 2; + min-width: 360px; +} + diff --git a/frontend/src/components/inputs/EntityForm.vue b/frontend/src/components/inputs/EntityForm.vue index 7eed02e..6840833 100644 --- a/frontend/src/components/inputs/EntityForm.vue +++ b/frontend/src/components/inputs/EntityForm.vue @@ -105,7 +105,7 @@ export default { }, entity: { type: Object, - required: true + required: false }, batchMode: { type: Boolean, diff --git a/frontend/src/components/layout/Tabbing.vue b/frontend/src/components/layout/Tabbing.vue index da1497a..f868953 100644 --- a/frontend/src/components/layout/Tabbing.vue +++ b/frontend/src/components/layout/Tabbing.vue @@ -12,17 +12,23 @@
- - - + + + +
- - diff --git a/frontend/src/composables/alert.js b/frontend/src/composables/alert.js new file mode 100644 index 0000000..8815150 --- /dev/null +++ b/frontend/src/composables/alert.js @@ -0,0 +1,44 @@ +import { randomUUID } from "@/utils"; + + +const ALERT_LEVELS = [ + 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'cta' +]; + +class AlertMessage { + constructor(level, msg) { + if (!ALERT_LEVELS.includes(level)){ + throw new TypeError("Invalid alert level."); + } + this.level = level; + this.message = msg; + this.id = `alert-${randomUUID()}`; + } +} + +class AlertStore { + constructor() { + this.storage = {} + } + + clear() { + for (const key in this.storage) { + delete this.storage[key]; + } + } + + push(level, msg) { + const alert = new AlertMessage(level, msg); + this.storage[alert.id] = alert; + } + + pop(alertId) { + delete this.storage[alertId]; + } + + get values() { + return Object.values(this.storage); + } +} + +export const alertStore = new AlertStore(); diff --git a/frontend/src/composables/api.js b/frontend/src/composables/api.js new file mode 100644 index 0000000..087226e --- /dev/null +++ b/frontend/src/composables/api.js @@ -0,0 +1,524 @@ +import { alertStore } from "../composables/alert"; + +class API { + constructor() { + this.base = "/api"; + this.storageprefix = "aimaas"; + this.alerts = alertStore; + } + + set token(val) { + if (val) { + window.localStorage.setItem(this.storageprefix + "Token", val); + } else { + window.localStorage.removeItem(this.storageprefix + "Token"); + } + } + + get token() { + return window.localStorage.getItem(this.storageprefix + "Token"); + } + + set loggedIn(val) { + if (val) { + window.localStorage.setItem(this.storageprefix + "User", val); + } else { + window.localStorage.removeItem(this.storageprefix + "User"); + } + } + + get loggedIn() { + return window.localStorage.getItem(this.storageprefix + "User"); + } + + set expires(val) { + if (!(val instanceof Date)) { + val = new Date(val); + } + if (val) { + window.localStorage.setItem(this.storageprefix + "Expires", val); + } else { + window.localStorage.removeItem(this.storageprefix + "Expires"); + } + } + + get expires() { + let val = window.localStorage.getItem(this.storageprefix + "Expires"); + if (!(val instanceof Date)) { + val = new Date(val); + } + return val; + } + + async _error_to_alert(details) { + const message = details?.message || "Failed to process request"; + if (this.alerts) { + this.alerts.push("danger", message); + } + console.error(message); + } + + async _is_response_ok(response) { + if (response.ok && response.status < 400) { + return null; + } + + if (response.status === 401 && this.token) { + // Token has expired. Automatic log out. + await this.logout(); + } + + let detail; + try { + detail = await response.json(); + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error(response.statusText); + } + throw error; + } + if (Array.isArray(detail.detail)) { + throw new Error( + detail.detail.map((d) => `${d.loc}: ${d.msg}`).join(", ") + ); + } else if (typeof detail.detail === "string") { + throw new Error(detail.detail); + } else { + console.error("Response indicates a problem", detail); + throw new Error( + "Failed to process request. See console for more details." + ); + } + } + + async _fetch({ url, headers, body, method, timeout_s = 10000 } = {}) { + let allheaders = { "Content-Type": "application/json" }; + const token = this.token; + if (token) { + allheaders["Authorization"] = `Bearer ${token}`; + } + let encoded_body = null; + allheaders = Object.assign(allheaders, headers || {}); + if (body instanceof FormData || typeof body === "string") { + encoded_body = body; + } else if (body) { + encoded_body = JSON.stringify(body); + } + + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeout_s); + + const response = await fetch(url, { + method: method || "GET", + body: encoded_body, + headers: allheaders, + signal: controller.signal, + }); + + clearTimeout(timeout); + await this._is_response_ok(response); + return await response.json(); + } catch (e) { + if (e.name === "AbortError") { + this._error_to_alert({ message: "Request timed out" }); + console.error("request timed out: %s", url); + } else { + this._error_to_alert(e); + } + return null; + } + } + + async getInfo() { + return this._fetch({ url: `${this.base}/info` }); + } + + async getSchemas({ all = false, deletedOnly = false } = {}) { + const params = new URLSearchParams(); + params.set("all", all); + params.set("deleted_only", deletedOnly); + return this._fetch({ url: `${this.base}/schema?${params.toString()}` }); + } + + async getSchema({ slugOrId } = {}) { + return this._fetch({ url: `${this.base}/schema/${slugOrId}` }); + } + + async createSchema({ body } = {}) { + const response = await this._fetch({ + url: `${this.base}/schema`, + method: "POST", + body: body, + }); + if (response !== null) { + this.alerts.push("success", `Schema created: ${body.name}`); + } + return response; + } + + async updateSchema({ schemaSlug, body } = {}) { + const response = await this._fetch({ + url: `${this.base}/schema/${schemaSlug}`, + method: "PUT", + body: body, + }); + if (response !== null) { + this.alerts.push("success", `Schema updated: ${schemaSlug}`); + } + return response; + } + + async deleteSchema({ slugOrId } = {}) { + const response = await this._fetch({ + url: `${this.base}/schema/${slugOrId}`, + method: "DELETE", + }); + if (response !== null) { + this.alerts.push("success", `Schema deleted: ${response.name}`); + } + return response; + } + + async getEntities({ + schemaSlug, + page = 1, + size = 10, + all = false, + deletedOnly = false, + allFields = false, + filters = {}, + orderBy = "name", + ascending = true, + } = {}) { + const params = new URLSearchParams(); + params.set("page", page); + params.set("size", size); + params.set("all", all); + params.set("all_fields", allFields); + params.set("deletedOnly", deletedOnly); + params.set("order_by", orderBy); + params.set("ascending", ascending); + for (const [filter, value] of Object.entries(filters)) { + params.set(filter, value); + } + return this._fetch({ + url: `${this.base}/entity/${schemaSlug}?${params.toString()}`, + }); + } + + async getEntity({ schemaSlug, entityIdOrSlug } = {}) { + const params = new URLSearchParams(); + return this._fetch({ + url: `${ + this.base + }/entity/${schemaSlug}/${entityIdOrSlug}?${params.toString()}`, + }); + } + + async createEntity({ schemaSlug, body } = {}) { + const response = await this._fetch({ + url: `${this.base}/entity/${schemaSlug}`, + method: "POST", + body: body, + }); + if (response !== null) { + this.alerts.push("success", `Entity created: ${body.name}`); + } + return response; + } + + async updateEntity({ schemaSlug, entityIdOrSlug, body } = {}) { + const response = await this._fetch({ + url: `${this.base}/entity/${schemaSlug}/${entityIdOrSlug}`, + method: "PUT", + body: body, + }); + if (response !== null) { + this.alerts.push("success", `Entity updated: ${entityIdOrSlug}`); + } + return response; + } + + async deleteEntity({ schemaSlug, entityIdOrSlug } = {}) { + const response = await this._fetch({ + url: `${this.base}/entity/${schemaSlug}/${entityIdOrSlug}`, + method: "DELETE", + }); + if (response !== null) { + this.alerts.push("success", `Entity deleted: ${response.name}`); + } + return response; + } + + async restoreEntity({ schemaSlug, entityIdOrSlug } = {}) { + const response = await this._fetch({ + url: `${this.base}/entity/${schemaSlug}/${entityIdOrSlug}?restore=1`, + method: "DELETE", + }); + if (response !== null) { + this.alerts.push("success", `Entity restored: ${response.name}`); + } + return response; + } + + async getChangeRequests({ + page = 1, + size = 10, + schemaSlug, + entityIdOrSlug, + } = {}) { + const params = new URLSearchParams(); + params.set("page", page); + params.set("size", size); + let url = `${this.base}/changes/schema/${schemaSlug}`; + if (entityIdOrSlug) { + url = `${this.base}/changes/entity/${schemaSlug}/${entityIdOrSlug}`; + } + return this._fetch({ url: `${url}?${params.toString()}` }); + } + + async getChangeRequestDetails({ objectType, changeId } = {}) { + let url = `${this.base}/changes/detail/${objectType}/${changeId}`; + return this._fetch({ url: url }); + } + + async getPendingChangeRequests({ page = 1, size = 50 } = {}) { + const params = new URLSearchParams(); + params.set("page", page); + params.set("size", size); + let url = `${this.base}/changes/pending?${params.toString()}`; + return this._fetch({ url: url }); + } + + async getCountOfPendingChangeRequests() { + return this._fetch({ url: `${this.base}/changes/pending/count` }); + } + + async reviewChanges({ changeId, verdict, comment = null } = {}) { + const response = await this._fetch({ + url: `${this.base}/changes/review/${changeId}`, + method: "POST", + body: { + result: verdict, + comment: comment || null, + }, + }); + if (response) { + this.alerts.push( + "success", + `Change request ${changeId} successfully reviewed.` + ); + } + return response; + } + + async login({ username, password } = {}) { + const url = `${this.base}/login`; + const body = new URLSearchParams({ + username: username, + password: password, + }); + const response = await this._fetch({ + url: url, + method: "POST", + body: body.toString(), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }); + if (response !== null) { + this.loggedIn = username; + this.token = response.access_token; + this.expires = response.expiration_date; + this.alerts.push("success", `Welcome back, ${username}.`); + } + return response; + } + + async logout() { + this.token = null; + this.loggedIn = null; + this.expires = null; + } + + async getUsers() { + return await this._fetch({ url: `${this.base}/users` }); + } + + async getUserMemberships({ username }) { + return await this._fetch({ + url: `${this.base}/users/${username}/memberships`, + }); + } + + async activate_user({ username }) { + const response = await this._fetch({ + url: `${this.base}/users/${username}`, + method: "PATCH", + }); + if (response !== null) { + this.alerts.push("success", `User activated: ${username}`); + } + return response; + } + + async deactivate_user({ username }) { + const response = await this._fetch({ + url: `${this.base}/users/${username}`, + method: "DELETE", + }); + if (response !== null) { + this.alerts.push("success", `User deactivated: ${username}`); + } + return response; + } + + async getGroups() { + return await this._fetch({ url: `${this.base}/groups` }); + } + + async getMembers({ groupId }) { + return await this._fetch({ url: `${this.base}/groups/${groupId}/members` }); + } + + async createGroup({ body }) { + + const response = await this._fetch({ + url: `${this.base}/groups`, + body: body, + method: "POST", + }); + if (response !== null) { + this.alerts.push("success", `Created new group: ${response.name}`); + } + return response; + } + + async updateGroup({ groupId, body }) { + const response = await this._fetch({ + url: `${this.base}/groups/${groupId}`, + body: body, + method: "PUT", + }); + if (response !== null) { + this.alerts.push( + "success", + `Changes to group have been saved: ${body.name}` + ); + } + return response; + } + + async deleteGroup({ groupId }) { + const response = await this._fetch({ + url: `${this.base}/groups/${groupId}`, + method: "DELETE", + }); + if (response !== null) { + this.alerts.push("success", "Group has been deleted"); + } + return response; + } + + async addMembers({ groupId, userIds }) { + const response = await this._fetch({ + url: `${this.base}/groups/${groupId}/members`, + body: userIds, + method: "PATCH", + }); + if (response !== null) { + if (response) { + this.alerts.push("success", "New members were added to group."); + } else { + this.alerts.push( + "warning", + "No new members added to group because requested users are already members." + ); + } + } + return response; + } + + async removeMembers({ groupId, userIds }) { + const response = await this._fetch({ + url: `${this.base}/groups/${groupId}/members`, + body: userIds, + method: "DELETE", + }); + if (response !== null) { + if (response) { + this.alerts.push("success", "Members were removed from group."); + } else { + this.alerts.push( + "warning", + "No members were removed from group because requested users were no members." + ); + } + } + return response; + } + + async getPermissions({ + recipientType = null, + recipientId = null, + objType = null, + objId = null, + }) { + const params = new URLSearchParams(); + if (recipientId) { + params.set("recipient_id", recipientId); + } + if (recipientType) { + params.set("recipient_type", recipientType); + } + if (objType) { + params.set("obj_type", objType); + } + if (objId) { + params.set("obj_id", objId); + } + return await this._fetch({ + url: `${this.base}/permissions?${params.toString()}`, + }); + } + + async grantPermission({ + recipientType, + recipientName, + objType, + objId, + permission, + }) { + const response = await this._fetch({ + url: `${this.base}/permissions`, + method: "POST", + body: { + recipient_type: recipientType, + recipient_name: recipientName, + obj_type: objType, + obj_id: objId, + permission: permission, + }, + }); + if (response) { + this.alerts.push("success", "Permission granted."); + } else { + this.alerts.push("warning", "Granting of permission not possible."); + } + } + + async revokePermissions({ permissionIds }) { + const response = this._fetch({ + url: `${this.base}/permissions`, + body: permissionIds, + method: "DELETE", + }); + if (response != null) { + this.alerts.push("success", "Selected permissions revoked."); + } else { + this.alerts.push("warning", "Not able to revoke selected permissions."); + } + return response; + } +} + +export const api = new API(); diff --git a/frontend/src/composables/auth.js b/frontend/src/composables/auth.js deleted file mode 100644 index 44ff651..0000000 --- a/frontend/src/composables/auth.js +++ /dev/null @@ -1,25 +0,0 @@ -export async function loadGroupData(api) { - const response = await api.getGroups(); - const groups = {}; - const tree = {}; - - for (let group of (response || [])) { - groups[group.id] = group; - if (!(group.parent_id in tree)) { - tree[group.parent_id] = []; - } - tree[group.parent_id].push(group.id); - } - - return [groups, tree]; -} - - -export async function loadUserData(api) { - const response = await api.getUsers(); - const users = {}; - for (const user of (response || [])) { - users[user.id] = user; - } - return users; -} diff --git a/frontend/src/plugins/alert.js b/frontend/src/plugins/alert.js index 046aa89..d8edddc 100644 --- a/frontend/src/plugins/alert.js +++ b/frontend/src/plugins/alert.js @@ -1,48 +1,7 @@ -import {randomUUID} from "@/utils"; - - -const ALERT_LEVELS = [ - 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'cta' -]; - -class AlertMessage { - constructor(level, msg) { - if (!ALERT_LEVELS.includes(level)){ - throw new TypeError("Invalid alert level."); - } - this.level = level; - this.message = msg; - this.id = `alert-${randomUUID()}`; - } -} - -class AlertStore { - constructor() { - this.storage = {} - } - - clear() { - for (const key in this.storage) { - delete this.storage[key]; - } - } - - push(level, msg) { - const alert = new AlertMessage(level, msg); - this.storage[alert.id] = alert; - } - - pop(alertId) { - delete this.storage[alertId]; - } - - get values() { - return Object.values(this.storage); - } -} +import { alertStore } from "../composables/alert"; export default { install: (app) => { - app.config.globalProperties.$alerts = new AlertStore(); + app.config.globalProperties.$alerts = alertStore; } -} \ No newline at end of file +} diff --git a/frontend/src/plugins/api.js b/frontend/src/plugins/api.js index 1565091..e5a674e 100644 --- a/frontend/src/plugins/api.js +++ b/frontend/src/plugins/api.js @@ -1,495 +1,8 @@ -import {reactive} from "vue"; - - -class API { - constructor(app) { - this.base = "/api"; - this.storageprefix = "aimaas"; - this.app = app; - this.alerts = app.config.globalProperties.$alerts; - } - - set token(val) { - if (val) { - window.localStorage.setItem(this.storageprefix + "Token", val); - } else { - window.localStorage.removeItem(this.storageprefix + "Token"); - } - } - - get token() { - return window.localStorage.getItem(this.storageprefix + "Token"); - } - - set loggedIn(val) { - if (val) { - window.localStorage.setItem(this.storageprefix + "User", val); - } else { - window.localStorage.removeItem(this.storageprefix + "User"); - } - } - - get loggedIn() { - return window.localStorage.getItem(this.storageprefix + "User"); - } - - set expires(val) { - if (!(val instanceof Date)) { - val = new Date(val); - } - if (val) { - window.localStorage.setItem(this.storageprefix + "Expires", val); - } else { - window.localStorage.removeItem(this.storageprefix + "Expires"); - } - } - - get expires() { - let val = window.localStorage.getItem(this.storageprefix + "Expires"); - if (!(val instanceof Date)) { - val = new Date(val); - } - return val - } - - async _error_to_alert(details) { - const message = details?.message || "Failed to process request"; - if (this.alerts) { - this.alerts.push("danger", message); - } - console.error(message); - } - - async _is_response_ok(response) { - if (response.ok && response.status < 400) { - return null; - } - - if (response.status === 401 && this.token) { - // Token has expired. Automatic log out. - await this.logout(); - } - - let detail; - try { - detail = await response.json(); - } catch (error) { - if (error instanceof SyntaxError) { - throw new Error(response.statusText); - } - throw error; - } - if (Array.isArray(detail.detail)) { - throw new Error(detail.detail.map(d => `${d.loc}: ${d.msg}`).join(", ")); - } else if (typeof detail.detail === "string") { - throw new Error(detail.detail); - } else { - console.error("Response indicates a problem", detail); - throw new Error("Failed to process request. See console for more details."); - } - } - - async _fetch({url, headers, body, method, timeout_s = 10000} = {}) { - let allheaders = {"Content-Type": "application/json"}; - const token = this.token; - if (token) { - allheaders["Authorization"] = `Bearer ${token}`; - } - let encoded_body = null; - allheaders = Object.assign(allheaders, headers || {}); - if (body instanceof FormData || typeof body === "string") { - encoded_body = body; - } else if (body) { - encoded_body = JSON.stringify(body); - } - - try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeout_s); - - const response = await fetch(url, { - method: method || 'GET', - body: encoded_body, - headers: allheaders, - signal: controller.signal, - }); - - clearTimeout(timeout); - await this._is_response_ok(response); - return await response.json(); - } catch (e) { - if(e.name === "AbortError") { - this._error_to_alert({message: "Request timed out"}); - console.error("request timed out: %s", url); - } else { - this._error_to_alert(e); - } - return null; - } - } - - async getInfo() { - return this._fetch({url: `${this.base}/info`}); - } - - async getSchemas({all = false, deletedOnly = false} = {}) { - const params = new URLSearchParams(); - params.set('all', all); - params.set('deleted_only', deletedOnly); - return this._fetch({url: `${this.base}/schema?${params.toString()}`}); - } - - async getSchema({slugOrId} = {}) { - return this._fetch({url: `${this.base}/schema/${slugOrId}`}); - } - - async createSchema({body} = {}) { - const response = await this._fetch({ - url: `${this.base}/schema`, - method: 'POST', - body: body, - }); - if (response !== null) { - this.alerts.push("success", `Schema created: ${body.name}`); - } - return response; - } - - async updateSchema({schemaSlug, body} = {}) { - const response = await this._fetch({ - url: `${this.base}/schema/${schemaSlug}`, - method: "PUT", - body: body - }); - if (response !== null) { - this.alerts.push("success", `Schema updated: ${schemaSlug}`); - } - return response; - } - - async deleteSchema({slugOrId} = {}) { - const response = await this._fetch({ - url: `${this.base}/schema/${slugOrId}`, - method: 'DELETE' - }); - if (response !== null) { - this.alerts.push("success", `Schema deleted: ${response.name}`); - } - return response; - } - - async getEntities({ - schemaSlug, - page = 1, - size = 10, - all = false, - deletedOnly = false, - allFields = false, - filters = {}, - orderBy = 'name', - ascending = true, - } = {}) { - const params = new URLSearchParams(); - params.set('page', page); - params.set('size', size); - params.set('all', all); - params.set('all_fields', allFields); - params.set('deletedOnly', deletedOnly); - params.set('order_by', orderBy); - params.set('ascending', ascending); - for (const [filter, value] of Object.entries(filters)) { - params.set(filter, value); - } - return this._fetch({ - url: `${this.base}/entity/${schemaSlug}?${params.toString()}` - }); - } - - async getEntity({schemaSlug, entityIdOrSlug} = {}) { - const params = new URLSearchParams(); - return this._fetch({ - url: `${this.base}/entity/${schemaSlug}/${entityIdOrSlug}?${params.toString()}` - }); - } - - async createEntity({schemaSlug, body} = {}) { - const response = await this._fetch({ - url: `${this.base}/entity/${schemaSlug}`, - method: 'POST', - body: body - }); - if (response !== null) { - this.alerts.push("success", `Entity created: ${body.name}`); - } - return response; - } - - async updateEntity({schemaSlug, entityIdOrSlug, body} = {}) { - const response = await this._fetch({ - url: `${this.base}/entity/${schemaSlug}/${entityIdOrSlug}`, - method: 'PUT', - body: body - }); - if (response !== null) { - this.alerts.push("success", `Entity updated: ${entityIdOrSlug}`); - } - return response; - } - - async deleteEntity({schemaSlug, entityIdOrSlug} = {}) { - const response = await this._fetch({ - url: `${this.base}/entity/${schemaSlug}/${entityIdOrSlug}`, - method: 'DELETE' - }); - if (response !== null) { - this.alerts.push("success", `Entity deleted: ${response.name}`); - } - return response; - } - - - async restoreEntity({schemaSlug, entityIdOrSlug} = {}){ - const response = await this._fetch({ - url: `${this.base}/entity/${schemaSlug}/${entityIdOrSlug}?restore=1`, - method: 'DELETE' - }); - if (response !== null) { - this.alerts.push("success", `Entity restored: ${response.name}`); - } - return response; - } - - async getChangeRequests({page = 1, size = 10, schemaSlug, entityIdOrSlug} = {}) { - const params = new URLSearchParams(); - params.set('page', page); - params.set('size', size); - let url = `${this.base}/changes/schema/${schemaSlug}`; - if (entityIdOrSlug) { - url = `${this.base}/changes/entity/${schemaSlug}/${entityIdOrSlug}`; - } - return this._fetch({url: `${url}?${params.toString()}`}); - } - - async getChangeRequestDetails({objectType, changeId} = {}) { - let url = `${this.base}/changes/detail/${objectType}/${changeId}`; - return this._fetch({url: url,}) - } - - async getPendingChangeRequests({page = 1, size = 50} = {}) { - const params = new URLSearchParams(); - params.set('page', page); - params.set('size', size); - let url = `${this.base}/changes/pending?${params.toString()}`; - return this._fetch({url: url}); - } - - async getCountOfPendingChangeRequests() { - return this._fetch({url: `${this.base}/changes/pending/count`,}); - } - - async reviewChanges({changeId, verdict, comment=null} = {}) { - const response = await this._fetch({ - url: `${this.base}/changes/review/${changeId}`, - method: "POST", - body: { - result: verdict, - comment: comment || null, - } - }); - if (response) { - this.alerts.push("success", `Change request ${changeId} successfully reviewed.`); - } - return response; - } - - async login({username, password} = {}) { - const url = `${this.base}/login`; - const body = new URLSearchParams({ - username: username, - password: password - }); - const response = await this._fetch({ - url: url, - method: 'POST', - body: body.toString(), - headers: {'Content-Type': 'application/x-www-form-urlencoded'} - }); - if (response !== null) { - this.loggedIn = username; - this.token = response.access_token; - this.expires = response.expiration_date; - this.alerts.push("success", `Welcome back, ${username}.`); - } - return response; - } - - async logout() { - this.token = null; - this.loggedIn = null; - this.expires = null; - } - - async getUsers() { - return await this._fetch({url: `${this.base}/users`}); - } - - async getUserMemberships({username}) { - return await this._fetch({ - url: `${this.base}/users/${username}/memberships` - }) ; - } - - async activate_user({username}) { - const response = await this._fetch({ - url: `${this.base}/users/${username}`, - method: "PATCH" - }); - if (response !== null) { - this.alerts.push("success", `User activated: ${username}`); - } - return response - } - - async deactivate_user({username}) { - const response = await this._fetch({ - url: `${this.base}/users/${username}`, - method: "DELETE" - }); - if (response !== null) { - this.alerts.push("success", `User deactivated: ${username}`); - } - return response - } - - async getGroups() { - return await this._fetch({url: `${this.base}/groups`}); - } - - async getMembers({groupId}) { - return await this._fetch({url: `${this.base}/groups/${groupId}/members`}); - } - - async createGroup({body}) { - const response = await this._fetch({ - url: `${this.base}/groups`, - body: body, - method: "POST" - }); - if (response !== null) { - this.alerts.push("success", `Created new group: ${response.name}`); - } - return response; - } - - async updateGroup({groupId, body}) { - const response = await this._fetch({ - url: `${this.base}/groups/${groupId}`, - body: body, - method: "PUT" - }); - if (response !== null) { - this.alerts.push("success", `Changes to group have been saved: ${body.name}`) - } - return response; - } - - async deleteGroup({groupId}) { - const response = await this._fetch({ - url: `${this.base}/groups/${groupId}`, - method: 'DELETE' - }); - if (response !== null) { - this.alerts.push("success", "Group has been deleted"); - } - return response; - } - - async addMembers({groupId, userIds}) { - const response = await this._fetch({ - url: `${this.base}/groups/${groupId}/members`, - body: userIds, - method: "PATCH" - }); - if (response !== null) { - if (response) { - this.alerts.push("success", "New members were added to group."); - } else { - this.alerts.push("warning", "No new members added to group because requested users are already members."); - } - } - return response; - } - - async removeMembers({groupId, userIds}) { - const response = await this._fetch({ - url: `${this.base}/groups/${groupId}/members`, - body: userIds, - method: "DELETE" - }); - if (response !== null) { - if (response) { - this.alerts.push("success", "Members were removed from group."); - } else { - this.alerts.push("warning", "No members were removed from group because requested users were no members."); - } - } - return response; - } - - async getPermissions({recipientType=null, recipientId=null, objType=null, objId=null}) { - const params = new URLSearchParams(); - if (recipientId) { - params.set("recipient_id", recipientId); - } - if (recipientType) { - params.set("recipient_type", recipientType); - } - if (objType) { - params.set("obj_type", objType); - } - if (objId) { - params.set("obj_id", objId); - } - return await this._fetch({url: `${this.base}/permissions?${params.toString()}`}); - } - - async grantPermission({recipientType, recipientName, objType, objId, permission}) { - const response = await this._fetch({ - url: `${this.base}/permissions`, - method: 'POST', - body: { - recipient_type: recipientType, - recipient_name: recipientName, - obj_type: objType, - obj_id: objId, - permission: permission - } - }) - if (response) { - this.alerts.push("success", "Permission granted.") - } else { - this.alerts.push("warning", "Granting of permission not possible.") - } - } - - async revokePermissions({permissionIds}) { - const response = this._fetch({ - url: `${this.base}/permissions`, - body: permissionIds, - method: 'DELETE' - }); - if (response != null) { - this.alerts.push("success", "Selected permissions revoked."); - } else { - this.alerts.push("warning", "Not able to revoke selected permissions."); - } - return response; - } -} - +import { reactive } from "vue"; +import { api } from "../composables/api"; export default { install: (app) => { - app.config.globalProperties.$api = reactive(new API(app)); + app.config.globalProperties.$api = reactive(api); } } diff --git a/frontend/src/store/auth.js b/frontend/src/store/auth.js new file mode 100644 index 0000000..d5b904e --- /dev/null +++ b/frontend/src/store/auth.js @@ -0,0 +1,96 @@ +import { ref,computed } from 'vue'; +import { api } from '@/composables/api'; + +const groups = ref({}); +const users = ref({}); + +export const useAuthStore = () => { + + const tree = computed(() => { + const computedTree = {}; + for (let group of Object.values(groups.value)) { + if (!(group.parent_id in computedTree)) { + computedTree[group.parent_id] = []; + } + + if (computedTree[group.parent_id].indexOf(group.id) < 0 ) { + computedTree[group.parent_id].push(group.id); + } + } + + return computedTree; + }) + + const loadGroupData = async () => { + const response = await api.getGroups(); + + for (let group of (response || [])) { + groups.value[group.id] = group; + } + } + + const updateGroup = async (groupId, groupData) => { + try { + const response = await api.updateGroup({ groupId: groupId, body: groupData }); + groups.value[response.id] = response; + } catch (err) { + console.error(err); + } + } + + const createGroup = async (groupData) => { + try { + const response = await api.createGroup({ body: groupData }); + groups.value[response.id] = response; + } catch (err) { + console.error(err); + } + } + + const deleteGroup = async (groupId) => { + const response = await api.deleteGroup({ + groupId: groupId, + }); + if (response) { + delete groups.value[groupId] + } + } + + const loadUserData = async () => { + const response = await api.getUsers(); + for (const user of (response || [])) { + users.value[user.id] = user; + } + } + + const activateUser = async (username) => { + await api.activate_user({username: username}); + Object.values(users.value).forEach(user => { + if(user.username === username) { + user.is_active = !user.is_active + } + }); + } + + const deactivateUser = async (username) => { + await api.deactivate_user({username: username}); + Object.values(users.value).forEach(user => { + if(user.username === username) { + user.is_active = !user.is_active + } + }); + } + + return { + groups, + users, + tree, + loadGroupData, + createGroup, + updateGroup, + deleteGroup, + loadUserData, + activateUser, + deactivateUser, + } +} From c26d14783bf133504c09ba47cd9ceaf257229ba5 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 3 Apr 2024 16:13:27 +0200 Subject: [PATCH 2/2] fixup! AIMAAS #197: refactor users/groups state management --- frontend/src/components/Schema.vue | 12 ++--- frontend/src/components/SearchPanel.vue | 4 +- frontend/src/components/auth/GroupManager.vue | 29 ++++++------ .../src/components/auth/GroupMemberships.vue | 5 -- frontend/src/components/auth/UserDetails.vue | 46 +++++++++++-------- frontend/src/components/auth/UserManager.vue | 6 +-- .../src/components/change_review/Changes.vue | 4 +- frontend/src/components/layout/Tabbing.vue | 1 - frontend/src/composables/api.js | 16 +++++-- frontend/src/store/auth.js | 44 ++++++++++++------ 10 files changed, 96 insertions(+), 71 deletions(-) diff --git a/frontend/src/components/Schema.vue b/frontend/src/components/Schema.vue index e4d2d53..7944489 100644 --- a/frontend/src/components/Schema.vue +++ b/frontend/src/components/Schema.vue @@ -16,7 +16,7 @@ diff --git a/frontend/src/components/auth/UserDetails.vue b/frontend/src/components/auth/UserDetails.vue index b2fac37..035e662 100644 --- a/frontend/src/components/auth/UserDetails.vue +++ b/frontend/src/components/auth/UserDetails.vue @@ -10,22 +10,32 @@
Is Active?
- - {{ user?.is_active ? 'Yes' : 'No' }} + + {{ user?.is_active ? "Yes" : "No" }}
-
@@ -49,12 +59,12 @@ export default { props: { user: { required: false, - type: Object - } + type: Object, + }, }, setup() { - const { activateUser, deactivateUser} = useAuthStore(); - return { activateUser, deactivateUser } + const { activateUser, deactivateUser } = useAuthStore(); + return { activateUser, deactivateUser }; }, methods: { async onClick() { @@ -62,15 +72,13 @@ export default { return; } if (this.user?.is_active) { - await this.deactivateUser(this.user.username) + await this.deactivateUser(this.user.username); } else { - await this.activateUser(this.user.username) + await this.activateUser(this.user.username); } - } - } -} + }, + }, +}; - \ No newline at end of file + diff --git a/frontend/src/components/auth/UserManager.vue b/frontend/src/components/auth/UserManager.vue index 8690ed9..2b16407 100644 --- a/frontend/src/components/auth/UserManager.vue +++ b/frontend/src/components/auth/UserManager.vue @@ -17,11 +17,9 @@ -
+

{{ selectedUser?.username}}

- - - +
diff --git a/frontend/src/components/change_review/Changes.vue b/frontend/src/components/change_review/Changes.vue index 4e6581a..4fc9f66 100644 --- a/frontend/src/components/change_review/Changes.vue +++ b/frontend/src/components/change_review/Changes.vue @@ -9,7 +9,7 @@