diff --git a/Changelog.md b/Changelog.md index b6cb293f..c7ad834e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,23 @@ ## [Unreleased] +## [3.9.0] - 2026-06-29 + +### Added +- Implemetned new design for role permission +- Implemented tenant access list + +## [3.8.0] - 2026-06-29 + +### Fixed +- Handled permission denied issue properly +- Rempved Quest from role +- Fixed issue of did update. It was giving sucess even if update failed + +### Added +- Implemented remove authenticator +- Implemented categorized role + ## [3.7.32] - 2026-06-20 diff --git a/package.json b/package.json index dac60d8d..da25f616 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "entity-developer-dashboard", - "version": "3.7.32", + "version": "3.9.0", "private": true, "scripts": { "serve": "vue-cli-service serve --mode production", diff --git a/src/App.vue b/src/App.vue index 8e4ccaca..1fd52152 100644 --- a/src/App.vue +++ b/src/App.vue @@ -422,7 +422,7 @@ color: #1a1a2e !important; - + Setup MFA @@ -615,6 +615,10 @@ export default { EventBus.$on("logoutAll", () => { this.logoutAll(); }); + + EventBus.$on("permissionDenied", (message) => { + this.notifyErr(message || "You don't have permission to perform this action."); + }); }, watch: { // Re-mount the sidebar on every navigation so that only the active diff --git a/src/assets/css/gblStyle.css b/src/assets/css/gblStyle.css index 588ef41e..700a3623 100644 --- a/src/assets/css/gblStyle.css +++ b/src/assets/css/gblStyle.css @@ -162,6 +162,7 @@ a { } + .badge-outline-success { color: #4ec96b; background-color: transparent; diff --git a/src/components/element/HfButtons.vue b/src/components/element/HfButtons.vue index 3bfe271a..816b89ae 100644 --- a/src/components/element/HfButtons.vue +++ b/src/components/element/HfButtons.vue @@ -4,11 +4,9 @@ depressed @click="emitExecuteAction()" > - - - - - {{name}} + {{ mdiIconName }} + + {{ name }} @@ -26,7 +24,8 @@ export default { default: ''}, name:{ type: String, - require:true + require:false, + default: '' }, iconClass: { type: String, @@ -42,13 +41,22 @@ export default { require:false } }, - computed:{ - // buttonThemeCss() { - // return { - // '--button-bg-color': config.app.buttonBgColor, - // '--button-text-color':config.app.buttonTextColor - // } - // }, + + computed: { + isMdi() { + return this.iconClass && (this.iconClass.startsWith('mdi') || this.iconClass.startsWith('mdi-')); + }, + mdiIconName() { + if (!this.iconClass) return ''; + const parts = this.iconClass.split(' '); + // prefer token that starts with 'mdi-' + const mdiDash = parts.find(p => p.startsWith('mdi-')); + if (mdiDash) return mdiDash; + // otherwise if 'mdi' exists, try to return next token that starts with 'mdi-' + const mdiIndex = parts.findIndex(p => p === 'mdi'); + if (mdiIndex >= 0 && parts[mdiIndex + 1]) return parts[mdiIndex + 1]; + return parts[0] || ''; + } }, methods:{ emitExecuteAction(){ @@ -58,7 +66,7 @@ export default { }; - diff --git a/src/components/login/mfa/SetupMfa.vue b/src/components/login/mfa/SetupMfa.vue index a1c46527..756fa7c1 100644 --- a/src/components/login/mfa/SetupMfa.vue +++ b/src/components/login/mfa/SetupMfa.vue @@ -71,6 +71,7 @@ import PIN from './PIN.vue' import { mapActions } from 'vuex/dist/vuex.common.js'; import UtilsMixin from "../../../mixins/utils"; +import { AUTHENTICATION_METHODS_LIST } from "../../../constants/authenticators"; export default { name: 'SetupMfa', @@ -79,18 +80,7 @@ export default { isLoading: false, qrCodeDataUrl: "", - authenticationMethodsList: [ - { - name: 'Google Authenticator', - value: 'google', - selected: true, - }, - { - name: 'Okta Authenticator', - value: 'okta', - selected: false - } - ], + authenticationMethodsList: AUTHENTICATION_METHODS_LIST, authenticationMethod: "", error: "" } @@ -140,8 +130,8 @@ export default { this.error = "Invalid code or expired, please try again" } else { this.notifySuccess(`Identity verified successfully`); - this.getMyUserDetails() - this.$emit("closePopup"); + const user = await this.getMyUserDetails() + this.$emit("closePopup", user); } this.isLoading = false } catch (e) { @@ -165,4 +155,4 @@ export default { ul { list-style-type: none; } - \ No newline at end of file + diff --git a/src/components/login/mfa/VerifyMfa.vue b/src/components/login/mfa/VerifyMfa.vue index 871a82c6..eea86054 100644 --- a/src/components/login/mfa/VerifyMfa.vue +++ b/src/components/login/mfa/VerifyMfa.vue @@ -69,24 +69,14 @@ import PIN from './PIN.vue' import { mapMutations, mapActions } from 'vuex/dist/vuex.common.js'; import UtilsMixin from "../../../mixins/utils"; import EventBus from "../../../eventbus"; +import { AUTHENTICATION_METHODS_LIST } from "../../../constants/authenticators"; export default { name: 'VerifyMfa', data() { return { isLoading: false, - authenticationMethodsList: [ - { - name: 'Okta Authenticator', - value: 'okta', - selected: false - }, - { - name: 'Google Authenticator', - value: 'google', - selected: false, - }, - ], + authenticationMethodsList: AUTHENTICATION_METHODS_LIST, authenticationMethod: '', error: "", } diff --git a/src/components/settings/UserProfile.vue b/src/components/settings/UserProfile.vue index c59d6d1f..8712f292 100644 --- a/src/components/settings/UserProfile.vue +++ b/src/components/settings/UserProfile.vue @@ -95,10 +95,13 @@ Authentication Methods - + text-color="black" + close + close-icon="mdi-close-circle" + @click:close="confirmRemoveAuth(auth)"> {{ auth.type }} @@ -122,10 +125,62 @@ - + + + +
+
+
+ +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ +
+ + {{ removeAuthError }} + +
+
+ + Cancel + + Remove +
+
+
+
+
@@ -243,6 +298,97 @@ justify-content: flex-end; } +/* confirm modal */ +.confirm-body { + padding: 0; +} + +.confirm-text { + font-size: 14px; + color: #374151; + line-height: 1.6; + margin-bottom: 20px; +} + +.confirm-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; +} + +.remove-auth-label { + display: block; + font-size: 14px; + color: #374151; + margin-bottom: 8px; +} + +.remove-auth-error { + color: red; +} + +.form-group label { + margin-bottom: 8px; + display: inline-block; +} +.remove-auth-card { + width: 100%; + margin: 0; + border-radius: 14px; + border: 1px solid #e5e7eb; + box-shadow: none; +} +.remove-auth-card .card-body { + padding: 20px; +} + +.form-group .tool-tip, +.form-group .tooltip-wrapper { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin-right: 4px; +} + +.form-group { + margin-bottom: 20px; +} + +/* Ensure select and PIN inputs inside the confirm modal have equal left/right spacing */ +.confirm-body .form-group > select.custom-select, +.confirm-body .form-group .pin-wrapper { + width: 100%; + box-sizing: border-box; + padding-left: 12px; + padding-right: 12px; +} + + +.confirm-body .form-group .pin-wrapper { + display: flex; + align-items: center; + gap: 8px; + padding-top: 6px; /* small top padding to match vertical rhythm */ + padding-bottom: 6px; +} + +/* Limit only the Remove Auth popup card width and center it (scoped) */ +.confirm-body .remove-auth-card { + max-width: 480px; + width: 100%; + margin: 0 auto; + box-sizing: border-box; +} + + + +/* Ensure images inside the confirm body are responsive */ +.confirm-body img { + max-width: 100%; + height: auto; +} + /* RESPONSIVE ADJUSTMENTS */ @media (max-width: 960px) { /* make avatar center & relative on small screens */ @@ -262,28 +408,51 @@ - diff --git a/src/components/stepper/StepCompanyDetails.vue b/src/components/stepper/StepCompanyDetails.vue index c72fa863..74448f9b 100644 --- a/src/components/stepper/StepCompanyDetails.vue +++ b/src/components/stepper/StepCompanyDetails.vue @@ -343,6 +343,7 @@ export default { COLLECT_WALLET: "Collect Wallet Address", AGE_VERIFICATION: "Age Verification", FRAUD_PREVENTION: "Fraud Prevention", + PROOF_OF_PERSONHOOD: "Proof of Personhood", }, BUSINESS_EST_YEARLY_VOLUME: { ZERO_ONEK: "0 - 1,000", @@ -539,7 +540,7 @@ export default { const c = this.localCompany; if (!c.name?.trim()) return this.showToast("Please enter a company/community name"); - if (!c.logo?.trim()) return this.showToast("Please provide a logo URL"); + if (!c.logo?.trim()) return this.showToast("Please upload logo."); if (c.type == this.BUSINESS_TYPE.BUSINESS && !c.country) return this.showToast("Please select a country"); if (c.type == this.BUSINESS_TYPE.BUSINESS && !c.registration_number) return this.showToast("Please enter your company registration number"); diff --git a/src/components/teams/AdminTeams.vue b/src/components/teams/AdminTeams.vue index bcf9a18f..f049e023 100644 --- a/src/components/teams/AdminTeams.vue +++ b/src/components/teams/AdminTeams.vue @@ -6,7 +6,7 @@
- + @@ -37,7 +37,7 @@ Edit + @click="promptDeleteRole(role._id, role.roleName)"> Delete
@@ -51,6 +51,27 @@ + + + + + mdi-alert-circle-outline + Delete Role + + + Are you sure you want to delete "{{ roleToDeleteName }}"? + All users linked with this role will lose access immediately. + + + + Cancel + + Delete + + + + +
@@ -65,25 +86,101 @@ Upto 200 chars -
-
    -
  • - - -
    - - + + +
    + +
    +
    +
    +
    {{ eachService.name }}
    +
    +
    {{ group.label }}
    +
    + + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    {{ eachService.name }}
    +
    +
    {{ group.label }}
    +
    + + +
    +
    -
  • -
-
+
+ + Save @@ -98,6 +195,148 @@ import { mapGetters, mapActions } from "vuex/dist/vuex.common.js"; import StudioSideBar from "../element/StudioSideBar.vue"; import UtilsMixin from "../../mixins/utils"; +import config from "../../config"; + +const PREDEFINED_ROLES = [ + { + key: "viewer", + name: "Viewer", + icon: "mdi-eye-outline", + description: "Minimal read-only access for business stakeholders.", + badge: "Limited view", + recommendedFor: "Founders, management, or external auditors", + permissions: ["READ_ANALYTICS", "READ_USAGE", "READ_COMPANY", "READ_COMPLIANCE"] + }, + { + key: "analyst", + name: "Analyst", + icon: "mdi-chart-line", + description: "Read-only access to verification, compliance, company, analytics, and usage data.", + badge: "Read-only", + recommendedFor: "Audit, support, or monitoring team", + permissions: ["READ_VERIFIED_USER", "READ_ANALYTICS", "READ_USAGE", "READ_COMPANY", "READ_COMPANY_EXECUTIVES", "READ_DOCUMENT", "READ_COMPLIANCE"] + }, + { + key: "finance", + name: "Finance", + icon: "mdi-cash-multiple", + description: "Can view usage, analytics, credits, and company billing-related information.", + badge: "Billing access", + recommendedFor: "Finance or accounts team", + permissions: ["READ_ANALYTICS", "READ_USAGE", "READ_CREDIT", "READ_COMPANY"] + }, + { + key: "developer", + name: "Developer", + icon: "mdi-code-tags", + description: "Can configure widgets and webhooks for technical integration.", + badge: "Integration access", + recommendedFor: "Engineering team", + permissions: ["READ_WIDGET_CONFIG", "WRITE_WIDGET_CONFIG", "UPDATE_WIDGET_CONFIG", "READ_WEBHOOK_CONFIG", "WRITE_WEBHOOK_CONFIG", "UPDATE_WEBHOOK_CONFIG", "DELETE_WEBHOOK_CONFIG", "READ_USAGE"] + }, + { + key: "compliance_manager", + name: "Compliance Manager", + icon: "mdi-shield-check-outline", + description: "Can review KYC/KYB cases, verify documents, and access compliance data.", + badge: "KYC/KYB review", + recommendedFor: "Compliance or operations team", + permissions: ["READ_VERIFIED_USER", "READ_ANALYTICS", "READ_USAGE", "READ_COMPANY", "UPDATE_COMPANY_STATUS", "READ_COMPANY_EXECUTIVES", "READ_DOCUMENT", "VERIFY_DOCUMENT", "READ_COMPLIANCE"] + }, + { + key: "admin", + name: "Admin", + icon: "mdi-shield-crown-outline", + description: "Full access to manage team, settings, verification, billing, and company account.", + badge: "Full access", + recommendedFor: "Organization owners / super admins", + permissions: ["READ_VERIFIED_USER", "READ_WIDGET_CONFIG", "WRITE_WIDGET_CONFIG", "UPDATE_WIDGET_CONFIG", "READ_WEBHOOK_CONFIG", "WRITE_WEBHOOK_CONFIG", "UPDATE_WEBHOOK_CONFIG", "DELETE_WEBHOOK_CONFIG", "READ_ANALYTICS", "READ_USAGE", "READ_CREDIT", "READ_COMPANY", "DELETE_COMPANY", "UPDATE_COMPANY_STATUS", "READ_COMPANY_EXECUTIVES", "READ_DOCUMENT", "VERIFY_DOCUMENT", "READ_COMPLIANCE"] + }, + { + key: "custom", + name: "Custom Role", + icon: "mdi-tune-variant", + description: "Start with no permissions and configure manually.", + badge: "Advanced", + recommendedFor: "Custom enterprise setups", + permissions: [] + } +]; + +const SSI_PREDEFINED_ROLES = [ + { + key: "auditor", + name: "Auditor", + icon: "mdi-clipboard-text-search-outline", + description: "Read-only access to SSI operations and verification records.", + badge: "Read-only", + recommendedFor: "Audit and compliance teams", + permissions: ["READ_DID", "READ_SCHEMA", "READ_CREDENTIAL", "READ_USAGE", "READ_TX"] + }, + { + key: "developer", + name: "Developer", + icon: "mdi-laptop", + description: "Technical integration and transaction monitoring access.", + badge: "Developer access", + recommendedFor: "Engineering teams", + permissions: ["READ_DID", "READ_SCHEMA", "READ_CREDENTIAL", "READ_USAGE", "READ_TX", "CHECK_LIVE_STATUS"] + }, + { + key: "verifier", + name: "Verifier", + icon: "mdi-check-decagram-outline", + description: "Can verify credentials, presentations, and DID signatures.", + badge: "Verification access", + recommendedFor: "Verification services", + permissions: ["READ_DID", "VERIFY_DID_SIGNATURE", "READ_SCHEMA", "READ_CREDENTIAL", "VERIFY_CREDENTIAL", "VERIFY_PRESENTATION", "READ_USAGE", "CHECK_LIVE_STATUS"] + }, + { + key: "credential_issuer", + name: "Credential Issuer", + icon: "mdi-card-account-details-outline", + description: "Can issue and manage credentials and presentations.", + badge: "Credential issuance", + recommendedFor: "Issuing authorities", + permissions: ["READ_DID", "VERIFY_DID_SIGNATURE", "READ_SCHEMA", "READ_CREDENTIAL", "WRITE_CREDENTIAL", "WRITE_PRESENTATION", "READ_USAGE", "CHECK_LIVE_STATUS"] + }, + { + key: "billing_manager", + name: "Billing Manager", + icon: "mdi-credit-card-settings-outline", + description: "Manage SSI credits and monitor service usage.", + badge: "Billing access", + recommendedFor: "Finance teams", + permissions: ["READ_CREDIT", "WRITE_CREDIT", "READ_USAGE", "CHECK_LIVE_STATUS"] + }, + { + key: "ssi_admin", + name: "SSI Admin", + icon: "mdi-shield-star-outline", + description: "Full access to DIDs, schemas, credentials, presentations, usage, and credits.", + badge: "Full access", + recommendedFor: "Organization owners / SSI admins", + permissions: ["ALL"] + }, + { + key: "identity_manager", + name: "Identity Manager", + icon: "mdi-identifier", + description: "Manage decentralized identifiers, schemas, and credential lifecycle.", + badge: "Identity management", + recommendedFor: "SSI operations teams", + permissions: ["READ_DID", "WRITE_DID", "VERIFY_DID_SIGNATURE", "ISSUE_DID_JWT", "READ_SCHEMA", "WRITE_SCHEMA", "READ_CREDENTIAL", "WRITE_CREDENTIAL", "VERIFY_CREDENTIAL", "WRITE_PRESENTATION", "VERIFY_PRESENTATION", "READ_USAGE", "READ_TX", "CHECK_LIVE_STATUS"] + }, + { + key: "custom", + name: "Custom Role", + icon: "mdi-tune-variant", + description: "Start with no permissions and configure manually.", + badge: "Advanced", + recommendedFor: "Custom enterprise setups", + permissions: [] + } +]; export default { name: "AdminTeams", @@ -107,12 +346,60 @@ export default { }, computed: { ...mapGetters("mainStore", ["getAllServices", "getAllRoles"]), - + PREDEFINED_ROLES() { + return PREDEFINED_ROLES; + }, + SSI_PREDEFINED_ROLES() { + return SSI_PREDEFINED_ROLES; + }, + categorizedServices() { + const ssiServices = this.localAllServices.filter(s => s.id === config.SERVICE_TYPES.SSI_API); + const idServices = this.localAllServices.filter( + s => s.id === config.SERVICE_TYPES.CAVACH_API || s.id === 'CAVACH_KYB_API' + ); + const otherServices = this.localAllServices.filter( + s => s.id !== config.SERVICE_TYPES.SSI_API && + s.id !== config.SERVICE_TYPES.CAVACH_API && + s.id !== 'CAVACH_KYB_API' + ); + const categories = []; + if (ssiServices.length) categories.push({ label: 'SSI Service', icon: 'mdi-link-variant', iconColor: '#3b82f6', services: ssiServices }); + if (idServices.length) categories.push({ label: 'ID Service', icon: 'mdi-shield-account-outline', iconColor: '#10b981', services: idServices }); + if (otherServices.length) categories.push({ label: 'Other Services', icon: 'mdi-apps', iconColor: '#6b7280', services: otherServices }); + return categories; + }, + hasSSIService() { + return this.localAllServices.some(s => s.id === config.SERVICE_TYPES.SSI_API); + }, + hasIDService() { + return this.localAllServices.some( + s => s.id !== config.SERVICE_TYPES.SSI_API && s.id !== config.SERVICE_TYPES.QUEST + ); + }, + ssiServices() { + return this.localAllServices.filter(s => s.id === config.SERVICE_TYPES.SSI_API); + }, + idServices() { + return this.localAllServices.filter( + s => s.id !== config.SERVICE_TYPES.SSI_API && s.id !== config.SERVICE_TYPES.QUEST + ); + } + }, + watch: { + getAllServices: { + handler(services) { + this.localAllServices = this.getRoleServices(services); + }, + immediate: true + } }, data() { return { isLoading: false, edit: false, + confirmDialog: false, + roleToDelete: null, + roleToDeleteName: '', roleModel: { "roleName": "", "roleDescription": "", @@ -121,7 +408,11 @@ export default { "servicePermissions": [] }, localAllServices: [], - checked: true + checked: true, + selectedRoles: { + id: 'viewer', + ssi: 'auditor' + } } }, mounted() { @@ -129,10 +420,52 @@ export default { this.fetchServicesList() } - this.localAllServices = this.getAllServices + this.localAllServices = this.getRoleServices(this.getAllServices) }, methods: { ...mapActions("mainStore", ["getMyRolesAction", "createARole", "deleteARole", "fetchServicesList", "updateARole",]), + getRoleServices(services = []) { + return services.filter(service => service.id !== config.SERVICE_TYPES.QUEST); + }, + getPermSubGroups(serviceId, accessList) { + const allKeys = Object.keys(accessList); + const SSI_GROUPS = [ + { label: 'General', icon: 'mdi-star-outline', iconColor: '#6366f1', keys: ['ALL'] }, + { label: 'DID', icon: 'mdi-identifier', iconColor: '#3b82f6', keys: ['READ_DID', 'WRITE_DID', 'VERIFY_DID_SIGNATURE', 'ISSUE_DID_JWT'] }, + { label: 'Schema', icon: 'mdi-file-tree-outline', iconColor: '#8b5cf6', keys: ['READ_SCHEMA', 'WRITE_SCHEMA'] }, + { label: 'Credential', icon: 'mdi-card-account-details-outline', iconColor: '#059669', keys: ['READ_CREDENTIAL', 'VERIFY_CREDENTIAL', 'WRITE_CREDENTIAL'] }, + { label: 'Presentation', icon: 'mdi-presentation', iconColor: '#0891b2', keys: ['WRITE_PRESENTATION', 'VERIFY_PRESENTATION'] }, + { label: 'Credit', icon: 'mdi-credit-card-outline', iconColor: '#d97706', keys: ['WRITE_CREDIT', 'READ_CREDIT'] }, + { label: 'Usage & Tx', icon: 'mdi-chart-line', iconColor: '#dc2626', keys: ['READ_USAGE', 'READ_TX', 'CHECK_LIVE_STATUS'] }, + ]; + const ID_GROUPS = [ + { label: 'General', icon: 'mdi-star-outline', iconColor: '#6366f1', keys: ['ALL'] }, + { label: 'Session & Users', icon: 'mdi-account-multiple-outline', iconColor: '#3b82f6', keys: ['READ_VERIFIED_USER'] }, + { label: 'Widget Config', icon: 'mdi-widgets-outline', iconColor: '#059669', keys: ['READ_WIDGET_CONFIG', 'WRITE_WIDGET_CONFIG', 'UPDATE_WIDGET_CONFIG'] }, + { label: 'Webhook Config', icon: 'mdi-webhook', iconColor: '#0891b2', keys: ['WRITE_WEBHOOK_CONFIG', 'READ_WEBHOOK_CONFIG', 'UPDATE_WEBHOOK_CONFIG', 'DELETE_WEBHOOK_CONFIG'] }, + { label: 'Analytics & Usage', icon: 'mdi-chart-bar', iconColor: '#d97706', keys: ['READ_ANALYTICS', 'READ_USAGE'] }, + { label: 'Credit', icon: 'mdi-credit-card-outline', iconColor: '#dc2626', keys: ['READ_CREDIT'] }, + { label: 'Company', icon: 'mdi-domain', iconColor: '#7c3aed', keys: ['READ_COMPANY', 'DELETE_COMPANY', 'UPDATE_COMPANY_STATUS'] }, + { label: 'Company Executives', icon: 'mdi-account-tie-outline', iconColor: '#065f46', keys: ['READ_COMPANY_EXECUTIVES'] }, + { label: 'Document', icon: 'mdi-file-document-outline', iconColor: '#92400e', keys: ['READ_DOCUMENT', 'VERIFY_DOCUMENT'] }, + { label: 'Compliance', icon: 'mdi-shield-check-outline', iconColor: '#1d4ed8', keys: ['READ_COMPLIANCE'] }, + ]; + const groupDefs = serviceId === config.SERVICE_TYPES.SSI_API ? SSI_GROUPS : ID_GROUPS; + const assignedKeys = new Set(); + const result = []; + for (const group of groupDefs) { + const perms = group.keys.filter(k => allKeys.includes(k)); + if (perms.length) { + result.push({ ...group, permissions: perms }); + perms.forEach(k => assignedKeys.add(k)); + } + } + // const remaining = allKeys.filter(k => !assignedKeys.has(k)); + // if (remaining.length) { + // result.push({ label: 'Other', icon: 'mdi-dots-horizontal', iconColor: '#6b7280', permissions: remaining }); + // } + return result; + }, createTeamPopup() { this.$root.$emit("bv::show::modal", "create-team"); }, @@ -144,10 +477,21 @@ export default { openSlider(action = 'add') { if (action == 'add') { this.resetData() + this.applyDefaultPredefinedRoles(); this.edit = false; this.$root.$emit("bv::toggle::collapse", "sidebar-right"); } }, + applyDefaultPredefinedRoles() { + if (this.hasIDService) { + const idDefault = PREDEFINED_ROLES.find(r => r.key === 'viewer'); + if (idDefault) this.selectPredefinedRole(idDefault, 'id'); + } + if (this.hasSSIService) { + const ssiDefault = SSI_PREDEFINED_ROLES.find(r => r.key === 'auditor'); + if (ssiDefault) this.selectPredefinedRole(ssiDefault, 'ssi'); + } + }, checkIfAccessIsThereInThatService(access, serviceId) { @@ -163,8 +507,59 @@ export default { this.resetData() this.edit = true; this.roleModel = { ...role }; + this.selectedRoles.id = this.detectSelectedPredefinedRole('id'); + this.selectedRoles.ssi = this.detectSelectedPredefinedRole('ssi'); this.$root.$emit("bv::toggle::collapse", "sidebar-right"); }, + getServiceIdsByType(serviceType) { + return (serviceType === 'ssi' ? this.ssiServices : this.idServices).map(s => s.id); + }, + detectSelectedPredefinedRole(serviceType) { + const serviceIds = new Set(this.getServiceIdsByType(serviceType)); + const currentPermissions = (this.roleModel.permissions || []) + .filter(p => serviceIds.has(p.serviceType)) + .map(p => p.access); + const currentSet = new Set(currentPermissions); + const roles = (serviceType === 'ssi' ? SSI_PREDEFINED_ROLES : PREDEFINED_ROLES).filter(r => r.key !== 'custom'); + if (!currentSet.size) return 'custom'; + const matched = roles.find(role => { + if (role.permissions.includes('ALL')) { + return currentSet.has('ALL'); + } + if (role.permissions.length !== currentSet.size) return false; + return role.permissions.every(p => currentSet.has(p)); + }); + return matched ? matched.key : 'custom'; + }, + selectPredefinedRole(role, serviceType) { + this.selectedRoles[serviceType] = role.key; + const serviceIds = new Set(this.getServiceIdsByType(serviceType)); + this.roleModel.permissions = (this.roleModel.permissions || []).filter(p => !serviceIds.has(p.serviceType)); + if (role.key === 'custom') { + return; + } + this.roleModel.permissions = [ + ...this.roleModel.permissions, + ...this.buildPermissionsForRole(role, serviceType) + ]; + }, + buildPermissionsForRole(role, serviceType) { + const permissions = []; + const targetServices = serviceType === 'ssi' ? this.ssiServices : this.idServices; + + targetServices.forEach(service => { + if (role.permissions.includes('ALL') && service.accessList.ALL !== undefined) { + permissions.push({ serviceType: service.id, access: 'ALL' }); + return; + } + role.permissions.forEach(access => { + if (service.accessList[access] !== undefined) { + permissions.push({ serviceType: service.id, access }); + } + }); + }); + return permissions; + }, closeSlider() { this.edit = false; @@ -172,14 +567,96 @@ export default { this.$root.$emit("bv::toggle::collapse", "sidebar-right"); }, - onCheck(event) { - const ev = event.target._value - if (ev) { - const index = this.roleModel.permissions.findIndex(x => ((x.serviceType == ev.serviceType) && (x.access == ev.access))) - if (index > -1) { - this.roleModel.permissions.splice(index, 1) - } else { - this.roleModel.permissions.push(ev) + isUpdateForcedByDelete(accessKey, serviceId) { + if (!accessKey.startsWith('UPDATE_')) return false; + const deleteKey = 'DELETE_' + accessKey.substring(7); // 'UPDATE_'.length = 7 + return this.checkIfAccessIsThereInThatService(deleteKey, serviceId); + }, + + isReadForcedByWrite(accessKey, serviceId) { + if (!accessKey.startsWith('READ_')) return false; + const writeKey = 'WRITE_' + accessKey.substring(5); + return this.checkIfAccessIsThereInThatService(writeKey, serviceId); + }, + + isWriteForcedByHigherPerm(accessKey, serviceId) { + if (!accessKey.startsWith('WRITE_')) return false; + const suffix = accessKey.substring(6); // 'WRITE_'.length = 6 + const deleteKey = 'DELETE_' + suffix; + const updateKey = 'UPDATE_' + suffix; + return this.checkIfAccessIsThereInThatService(deleteKey, serviceId) || + this.checkIfAccessIsThereInThatService(updateKey, serviceId); + }, + + onCheck(event, service) { + const ev = event.target._value; + if (!ev) return; + + if (ev.access === 'ALL') { + const allKeys = Object.keys(service.accessList); + this.roleModel.permissions = this.roleModel.permissions.filter(x => x.serviceType !== ev.serviceType); + if (event.target.checked) { + allKeys.forEach(key => { + this.roleModel.permissions.push({ serviceType: ev.serviceType, access: key }); + }); + } + return; + } + + const index = this.roleModel.permissions.findIndex(x => x.serviceType == ev.serviceType && x.access == ev.access); + if (index > -1) { + this.roleModel.permissions.splice(index, 1); + } else { + this.roleModel.permissions.push(ev); + } + + // Auto-manage READ when WRITE is toggled + if (ev.access.startsWith('WRITE_')) { + const readKey = 'READ_' + ev.access.substring(6); + if (service.accessList[readKey] !== undefined && event.target.checked) { + const readIdx = this.roleModel.permissions.findIndex( + x => x.serviceType == ev.serviceType && x.access == readKey + ); + if (readIdx === -1) { + this.roleModel.permissions.push({ serviceType: ev.serviceType, access: readKey }); + } + } + } + + // Auto-manage WRITE (and READ) when DELETE or UPDATE is toggled + if (ev.access.startsWith('DELETE_') || ev.access.startsWith('UPDATE_')) { + const suffix = ev.access.substring(7); // both 'DELETE_' and 'UPDATE_' are 7 chars + const writeKey = 'WRITE_' + suffix; + if (service.accessList[writeKey] !== undefined && event.target.checked) { + // Auto-add WRITE + const writeIdx = this.roleModel.permissions.findIndex( + x => x.serviceType == ev.serviceType && x.access == writeKey + ); + if (writeIdx === -1) { + this.roleModel.permissions.push({ serviceType: ev.serviceType, access: writeKey }); + } + // Auto-add READ (cascade) + const readKey = 'READ_' + suffix; + if (service.accessList[readKey] !== undefined) { + const readIdx = this.roleModel.permissions.findIndex( + x => x.serviceType == ev.serviceType && x.access == readKey + ); + if (readIdx === -1) { + this.roleModel.permissions.push({ serviceType: ev.serviceType, access: readKey }); + } + } + } + // Auto-add UPDATE when DELETE is toggled + if (ev.access.startsWith('DELETE_') && event.target.checked) { + const updateKey = 'UPDATE_' + suffix; + if (service.accessList[updateKey] !== undefined) { + const updateIdx = this.roleModel.permissions.findIndex( + x => x.serviceType == ev.serviceType && x.access == updateKey + ); + if (updateIdx === -1) { + this.roleModel.permissions.push({ serviceType: ev.serviceType, access: updateKey }); + } + } } } }, @@ -219,22 +696,24 @@ export default { } }, - async deleteThisRole(roleId) { + promptDeleteRole(roleId, roleName) { + this.roleToDelete = roleId; + this.roleToDeleteName = roleName; + this.confirmDialog = true; + }, + async confirmDeleteRole() { try { - const result = confirm('Are you sure you want to delete this role? Once deleted, all users linked with this role will loose access.') - if (result) { - // do all validations... - this.isLoading = true - await this.deleteARole(roleId) - this.isLoading = false - this.notifySuccess('Role is deleted successfully') - } - - + this.isLoading = true; + await this.deleteARole(this.roleToDelete); + this.isLoading = false; + this.confirmDialog = false; + this.roleToDelete = null; + this.roleToDeleteName = ''; + this.notifySuccess('Role deleted successfully'); } catch (e) { - this.notifyErr(e.message) - this.isLoading = false + this.notifyErr(e.message); + this.isLoading = false; } }, @@ -246,9 +725,142 @@ export default { ], "servicePermissions": [] } + this.selectedRoles = { id: 'viewer', ssi: 'auditor' }; this.edit = false; }, }, mixins: [UtilsMixin] } - \ No newline at end of file + + + diff --git a/src/components/teams/LiveAccessStatus.vue b/src/components/teams/LiveAccessStatus.vue new file mode 100644 index 00000000..387ffed6 --- /dev/null +++ b/src/components/teams/LiveAccessStatus.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/teams/MyInvitions.vue b/src/components/teams/MyInvitions.vue index 80da0afe..4541abc0 100644 --- a/src/components/teams/MyInvitions.vue +++ b/src/components/teams/MyInvitions.vue @@ -5,7 +5,7 @@
- + diff --git a/src/components/teams/TeamMembers.vue b/src/components/teams/TeamMembers.vue index 69bad0cd..31dc843e 100644 --- a/src/components/teams/TeamMembers.vue +++ b/src/components/teams/TeamMembers.vue @@ -10,7 +10,7 @@ --> - + diff --git a/src/components/teams/TeamUser.vue b/src/components/teams/TeamUser.vue index 5bf254c9..36f7b3f1 100644 --- a/src/components/teams/TeamUser.vue +++ b/src/components/teams/TeamUser.vue @@ -33,6 +33,17 @@ Pending + + + + + + diff --git a/src/views/Apps.vue b/src/views/Apps.vue index 2e80e8a7..aa7486ef 100644 --- a/src/views/Apps.vue +++ b/src/views/Apps.vue @@ -1221,6 +1221,7 @@ export default { this.$router.push({ name: "GettingStarted", + params: { appId }, }); break; } @@ -1405,9 +1406,6 @@ export default { }, async createAnApp() { try { - if (!this.appModel.whitelistCors) { - this.appModel.whitelistedCors = '*'; - } const errorMessages = this.validateFields(); if (errorMessages && errorMessages.message.length > 0) { throw errorMessages; diff --git a/src/views/SettingConfig.vue b/src/views/SettingConfig.vue index f0585ab3..cc075757 100644 --- a/src/views/SettingConfig.vue +++ b/src/views/SettingConfig.vue @@ -9,23 +9,6 @@ Profile - @@ -50,6 +33,13 @@ + + + + @@ -86,7 +76,7 @@ @@ -114,6 +104,7 @@ import TeamMembers from '../components/teams/TeamMembers.vue'; import MyInvitions from '../components/teams/MyInvitions.vue'; import AdminTeams from '../components/teams/AdminTeams.vue'; +import LiveAccessStatus from '../components/teams/LiveAccessStatus.vue'; import { mapMutations, mapGetters } from "vuex"; import OnlySSIApps from '../components/settings/OnlySSIApps.vue'; import UserProfile from '../components/settings/UserProfile.vue'; @@ -129,6 +120,7 @@ export default { components: { TeamMembers, AdminTeams, + LiveAccessStatus, MyInvitions, UserProfile, OnlySSIApps, @@ -153,32 +145,39 @@ export default { sentInvitiationCode: "" }; }, + watch: { + '$route.query.ref'(ref) { + this.applyRouteRef(ref); + } + }, methods: { ...mapMutations("mainStore", ["setMainSideNavBar"]), - ...mapActions('mainStore', ['getMyRolesAction', 'getPeopleMembers',]) + ...mapActions('mainStore', ['getMyRolesAction', 'getPeopleMembers',]), + applyRouteRef(ref) { + if (ref === 'invitions') { + this.$nextTick(() => { + this.activeMainTab = 2; + }) + } else if (ref === 'roles') { + this.activeMainTab = 1; + this.$nextTick(() => { + this.activeMembersSubTab = 1; + }); + } else if (ref === 'mfa') { + this.activeMainTab = 0; + this.$nextTick(() => { + this.activeProfileSubTab = 0; + }); + } else { + this.activeMainTab = 1; + this.activeMembersSubTab = 0; + } + } }, async mounted() { this.setMainSideNavBar(false); - const ref = this.$route.query.ref; - if (ref === 'invitions') { - this.$nextTick(() => { - this.activeMainTab = 2; - }) - } else if (ref === 'roles') { - this.activeMainTab = 1; - this.$nextTick(() => { - this.activeMembersSubTab = 1; - }); - } else if (ref === 'mfa') { - this.activeMainTab = 0; - this.$nextTick(() => { - this.activeProfileSubTab = 0; - }); - } else { - this.activeMainTab = 1; - this.activeMembersSubTab = 0; - } + this.applyRouteRef(this.$route.query.ref); try{ await this.getMyRolesAction() @@ -189,4 +188,4 @@ export default { } } }; - \ No newline at end of file + diff --git a/src/views/analytics/UserAnalytics.vue b/src/views/analytics/UserAnalytics.vue index 6801a37a..6ab0f631 100644 --- a/src/views/analytics/UserAnalytics.vue +++ b/src/views/analytics/UserAnalytics.vue @@ -1,5 +1,7 @@ \ No newline at end of file + diff --git a/src/views/analytics/components/DeviceStats.vue b/src/views/analytics/components/DeviceStats.vue index a3067007..dda7e9c6 100644 --- a/src/views/analytics/components/DeviceStats.vue +++ b/src/views/analytics/components/DeviceStats.vue @@ -55,6 +55,7 @@ \ No newline at end of file + diff --git a/src/views/playground/SSIDashboardUsages.vue b/src/views/playground/SSIDashboardUsages.vue index a34008a6..26460353 100644 --- a/src/views/playground/SSIDashboardUsages.vue +++ b/src/views/playground/SSIDashboardUsages.vue @@ -129,6 +129,9 @@ h5 span { + + +
@@ -186,14 +189,14 @@ h5 span {
-

+

API Consumptions

No usage found!

-
+
@@ -226,6 +229,7 @@ h5 span { + @@ -238,9 +242,12 @@ import { mapState, mapActions, mapMutations } from "vuex"; import { mapGetters } from 'vuex/dist/vuex.common.js'; import UtilsMixin from '../../mixins/utils'; +import AccessDenied from '../AccessDenied.vue'; +import { isAccessDeniedError } from '../../utils/accessDenied'; export default { name: "SSIDashboardCredit", components: { + AccessDenied }, computed: { @@ -394,6 +401,8 @@ export default { user: {}, fullPage: true, isLoading: false, + accessDenied: false, + accessDeniedMsg: '', startDate: "", endDate: "", @@ -432,6 +441,11 @@ export default { this.isLoading = true this.setDate() await this.fetchUsageForASSIService({ startDate: this.startDate, endDate: this.endDate }).then((data) => { + // fetchUsageForASSIService returns the error object instead of throwing on 403 + if (data && (data.statusCode >= 400 || data.error)) { + const msg = Array.isArray(data.message) ? data.message.join(', ') : (data.message || data.error || 'Access denied'); + throw new Error(msg); + } this.usageDetails = data; }) await this.fetchUsageDetailsForASSIService({ startDate: this.startDate, endDate: this.endDate }); @@ -440,8 +454,7 @@ export default { this.isLoading = false } catch (e) { this.isLoading = false - this.notifyErr(e.message) - this.$router.push({ path: '/studio/dashboard' }); + this.handleApiError(e, 'GET') } }, @@ -470,6 +483,23 @@ export default { ...mapMutations('playgroundStore', ['updateSideNavStatus', 'shiftContainer']), + handleApiError(error, method = 'GET') { + const message = typeof error === 'string' ? error : error?.message || 'Something went wrong'; + if (method.toUpperCase() === 'GET' && isAccessDeniedError(error)) { + this.accessDenied = true; + this.accessDeniedMsg = message; + return; + } + + this.notifyErr(message); + }, + + throwIfAccessDeniedResponse(data) { + if (data && (data.statusCode >= 400 || data.error)) { + const msg = Array.isArray(data.message) ? data.message.join(', ') : (data.message || data.error || 'Access denied'); + throw new Error(msg); + } + }, changeGraph(chartType) { if (chartType == 'line') { @@ -675,7 +705,9 @@ export default { this.endDate = (new Date(this.endDate)); this.isLoading = true - this.usageDetails = await this.fetchUsageForASSIService({ startDate: this.startDate, endDate: this.endDate }) + const usageDetails = await this.fetchUsageForASSIService({ startDate: this.startDate, endDate: this.endDate }) + this.throwIfAccessDeniedResponse(usageDetails); + this.usageDetails = usageDetails; await this.fetchUsageDetailsForASSIService({ startDate: this.startDate, endDate: this.endDate }); this.isLoading = false @@ -684,7 +716,7 @@ export default { this.renderUsageDetailsChart() } catch (e) { this.isLoading = false - this.notifyErr(e.message) + this.handleApiError(e, 'GET') } }, @@ -889,4 +921,4 @@ export default { } - \ No newline at end of file + diff --git a/src/views/playground/Schema.vue b/src/views/playground/Schema.vue index de0e63ed..ebdecd6b 100644 --- a/src/views/playground/Schema.vue +++ b/src/views/playground/Schema.vue @@ -140,7 +140,8 @@ @@ -149,11 +152,14 @@ import UtilsMixin from '../../../mixins/utils.js'; import { mapGetters, mapActions } from "vuex"; import HfButtons from '../../../components/element/HfButtons.vue'; import { isValidURL } from '../../../mixins/fieldValidation.js' +import AccessDenied from '../../AccessDenied.vue'; +import { isAccessDeniedError } from '../../../utils/accessDenied'; export default { name: "WEbhookConfig", mixins: [UtilsMixin], components: { HfButtons, + AccessDenied, }, computed: { ...mapGetters('mainStore', ['getWebhookConfig']), @@ -169,11 +175,7 @@ export default { this.isLoading = false } catch (e) { this.isLoading = false - console.error(e) - if (e.message) { - this.notifyErr(e.message) - } - // this.$router.push({ path: '/studio/dashboard' }); + return this.handleApiError(e, 'GET') } this.formatConfig() @@ -184,6 +186,8 @@ export default { return { fullPage: true, isLoading: false, + accessDenied: false, + accessDeniedMsg: '', headers: [ { key: "", value: "" }, // Initial header ], @@ -194,6 +198,16 @@ export default { }, methods: { ...mapActions('mainStore', ['fetchAppWebhookConfig', 'createAppWebhookConfig', 'deleteAppWebhookConfig', 'updateAppWebhookConfig']), + handleApiError(error, method = 'GET') { + const message = typeof error === 'string' ? error : error?.message || 'Something went wrong'; + if (method.toUpperCase() === 'GET' && isAccessDeniedError(error)) { + this.accessDenied = true; + this.accessDeniedMsg = message; + return; + } + + this.notifyErr(message) + }, addHeader() { this.headers.push({ key: "", value: "" }); }, diff --git a/src/views/sa/components/CreditRecharge.vue b/src/views/sa/components/CreditRecharge.vue index 67e599e5..0d4519d4 100644 --- a/src/views/sa/components/CreditRecharge.vue +++ b/src/views/sa/components/CreditRecharge.vue @@ -6,7 +6,9 @@

Service Credit Recharge

-
+ + +

Allocate credits and set validity periods for your registered backend services.

@@ -71,6 +73,33 @@