Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sidebar Re-design: prerequisites #4743

Merged
merged 1 commit into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function getPath (file) {

module.exports = function (env, argv) {
const config = {
devtool: process.env.mode === 'production' ? 'hidden-source-map' : 'source-map',
entry: {
main: getPath('frontend/src/main.js'),
setup: getPath('frontend/src/setup.js')
Expand Down
90 changes: 90 additions & 0 deletions frontend/src/composables/Permissions.js
joepavitt marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// import { useStore } from 'vuex'

import { Permissions } from '../../../forge/lib/permissions.js'
import { Roles } from '../utils/roles.js'
/**
* @typedef {0 | 5 | 10 | 30 | 50 | 99} Role
* Enum for roles with specific numeric values.
*/

export default function usePermissions () {
// todo There's a reactivity problem I can't figure out with the useStore utility and our account store when switching
// teams. The initialized store returns undefined upon switching teams and looses reactivity afterwards. This should
// warrant more investigations, as this usually happens when the entire store is re-written. The teamMembership arguments
// should be disposed asap

// const store = useStore()
// const teamMembership = store.state.account.teamMembership

/**
* @param teamMembership
* @returns {boolean}
*/
const isVisitingAdmin = (teamMembership) => {
return teamMembership === Roles.Admin
}

/**
*
* @param scope
* @param teamMembership
* @returns {boolean}
*/
const hasPermission = (scope, teamMembership) => {
if (!Permissions[scope]) {
throw new Error(`Unrecognised scope requested: '${scope}'`)
}
const permission = Permissions[scope]

if (permission.role) {
if (!teamMembership) {
return false
}
if (teamMembership.role < permission.role) {
return false
}
}
return true
}

/**
* Check if the user has the minimum required role.
* @param {Role} role - The role to check against.
* @param teamMembership
* @returns {boolean} True if the user has the minimum required role, otherwise false.
* @example
* // Check if the user has at least the 'Member' role
* const isMemberOrHigher = hasAMinimumTeamRoleOf(Roles.Member)
*/
const hasAMinimumTeamRoleOf = (role, teamMembership) => {
if (isVisitingAdmin(teamMembership?.role)) {
return true
}

return teamMembership?.role >= role
}

/**
* Check if the user has a lower role than given role.
* @param {Role} role - The role to check against.
* @param teamMembership
* @returns {boolean} True if the user has a lower role than the given one, otherwise false.
* @example
* // Check if the user has role lower than 'Member' role
* const isMemberOrHigher = hasALowerTeamRoleThan(Roles.Member)
*/
const hasALowerOrEqualTeamRoleThan = (role, teamMembership) => {
if (isVisitingAdmin(teamMembership?.role)) {
return true
}

return role <= teamMembership?.role
}

return {
isVisitingAdmin,
hasPermission,
hasAMinimumTeamRoleOf,
hasALowerOrEqualTeamRoleThan
}
}
58 changes: 32 additions & 26 deletions frontend/src/mixins/Features.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,82 @@
import { mapState } from 'vuex'
import { mapGetters, mapState } from 'vuex'

export default {
// todo The account store's featuresCheck getter should be used instead of this mixin
// Currently keeping it for backwards compat, as all permissions checks should use the accounts featuresCheck getter
computed: {
...mapState('account', ['features', 'team']),
...mapGetters('account', ['featuresCheck']),
isSharedLibraryFeatureEnabledForTeam () {
const flag = this.team.type.properties.features?.['shared-library']
return flag === undefined || flag
return this.featuresCheck.isSharedLibraryFeatureEnabledForTeam
},
isSharedLibraryFeatureEnabledForPlatform () {
return this.features['shared-library']
return this.featuresCheck.isSharedLibraryFeatureEnabledForPlatform
},
isSharedLibraryFeatureEnabled () {
return this.isSharedLibraryFeatureEnabledForTeam && this.isSharedLibraryFeatureEnabledForPlatform
return this.featuresCheck.isSharedLibraryFeatureEnabled
},
isBlueprintsFeatureEnabledForTeam () {
const flag = this.team.type.properties.features?.flowBlueprints
return flag === undefined || flag
return this.featuresCheck.isBlueprintsFeatureEnabledForTeam
},
isBlueprintsFeatureEnabledForPlatform () {
return this.features.flowBlueprints
return this.featuresCheck.isBlueprintsFeatureEnabledForPlatform
},
isBlueprintsFeatureEnabled () {
return this.isBlueprintsFeatureEnabledForTeam && this.isBlueprintsFeatureEnabledForPlatform
return this.featuresCheck.isBlueprintsFeatureEnabled
},
isCustomCatalogsFeatureEnabledForPlatform () {
return !!this.features.customCatalogs
return this.featuresCheck.isCustomCatalogsFeatureEnabledForPlatform
},
isCustomCatalogsFeatureEnabledForTeam () {
const flag = this.team.type.properties.features?.customCatalogs
return flag === undefined || flag
return this.featuresCheck.isCustomCatalogsFeatureEnabledForTeam
},
isCustomCatalogsFeatureEnabled () {
return this.isCustomCatalogsFeatureEnabledForPlatform && this.isCustomCatalogsFeatureEnabledForTeam
return this.featuresCheck.isCustomCatalogsFeatureEnabled
},
isStaticAssetFeatureEnabledForPlatform () {
return !!this.features.staticAssets
return this.featuresCheck.isStaticAssetFeatureEnabledForPlatform
},
isStaticAssetsFeatureEnabledForTeam () {
return !!this.team?.type?.properties?.features?.staticAssets
return this.featuresCheck.isStaticAssetsFeatureEnabledForTeam
},
isStaticAssetFeatureEnabled () {
return this.isStaticAssetFeatureEnabledForPlatform && this.isStaticAssetsFeatureEnabledForTeam
return this.featuresCheck.isStaticAssetFeatureEnabled
},
isHTTPBearerTokensFeatureEnabledForPlatform () {
return this.featuresCheck.isHTTPBearerTokensFeatureEnabledForPlatform
},
isHTTPBearerTokensFeatureEnabledForTeam () {
return this.settings?.features.httpBearerTokens && this.team.type.properties.features.teamHttpSecurity
return this.featuresCheck.isHTTPBearerTokensFeatureEnabledForTeam
},
isHTTPBearerTokensFeatureEnabled () {
return this.featuresCheck.isHTTPBearerTokensFeatureEnabled
},
isBOMFeatureEnabledForPlatform () {
return !!this.features.bom
return this.featuresCheck.isBOMFeatureEnabledForPlatform
},
isBOMFeatureEnabledForTeam () {
return !!this.team?.type?.properties?.features?.bom
return this.featuresCheck.isBOMFeatureEnabledForTeam
},
isBOMFeatureEnabled () {
return this.isBOMFeatureEnabledForPlatform && this.isBOMFeatureEnabledForTeam
return this.featuresCheck.isBOMFeatureEnabled
},
isTimelineFeatureEnabledForPlatform () {
return !!this.features.projectHistory
return this.featuresCheck.isTimelineFeatureEnabledForPlatform
},
isTimelineFeatureEnabledForTeam () {
return !!this.team?.type?.properties?.features?.projectHistory
return this.featuresCheck.isTimelineFeatureEnabledForTeam
},
isTimelineFeatureEnabled () {
return this.isTimelineFeatureEnabledForPlatform && this.isTimelineFeatureEnabledForTeam
return this.featuresCheck.isTimelineFeatureEnabled
},
isMqttBrokerFeatureEnabledForPlatform () {
return !!this.features.teamBroker
return this.featuresCheck.isMqttBrokerFeatureEnabledForPlatform
},
isMqttBrokerFeatureEnabledForTeam () {
return !!this.team?.type?.properties?.features?.teamBroker
return this.featuresCheck.isMqttBrokerFeatureEnabledForTeam
},
isMqttBrokerFeatureEnabled () {
return this.isMqttBrokerFeatureEnabledForPlatform && this.isMqttBrokerFeatureEnabledForTeam
return this.featuresCheck.isMqttBrokerFeatureEnabled
}
}
}
43 changes: 16 additions & 27 deletions frontend/src/mixins/Permissions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { mapState } from 'vuex'

import { Permissions } from '../../../forge/lib/permissions.js'
import { Roles } from '../utils/roles.js'
import usePermissions from '../composables/Permissions.js'

/**
* @typedef {0 | 5 | 10 | 30 | 50 | 99} Role
* Enum for roles with specific numeric values.
*/

/**
* @typedef {0 | 5 | 10 | 30 | 50 | 99} Role
* Enum for roles with specific numeric values.
Expand All @@ -10,26 +15,16 @@ import { Roles } from '../utils/roles.js'
export default {
computed: {
...mapState('account', ['team', 'teamMembership']),

isVisitingAdmin () {
return this.teamMembership?.role === Roles.Admin
const { isVisitingAdmin } = usePermissions()
return isVisitingAdmin(this.teamMembership?.role)
}
},
methods: {
hasPermission (scope) {
if (!Permissions[scope]) {
throw new Error(`Unrecognised scope requested: '${scope}'`)
}
const permission = Permissions[scope]
// if (<check settings>) {
if (permission.role) {
if (!this.teamMembership) {
return false
}
if (this.teamMembership.role < permission.role) {
return false
}
}
return true
const { hasPermission } = usePermissions()
return hasPermission(scope, this.teamMembership)
},

/**
Expand All @@ -41,11 +36,8 @@ export default {
* const isMemberOrHigher = hasAMinimumTeamRoleOf(Roles.Member)
*/
hasAMinimumTeamRoleOf (role) {
if (this.isVisitingAdmin) {
return true
}

return this.teamMembership?.role >= role
const { hasAMinimumTeamRoleOf } = usePermissions()
return hasAMinimumTeamRoleOf(role, this.teamMembership)
},

/**
Expand All @@ -57,11 +49,8 @@ export default {
* const isMemberOrHigher = hasALowerTeamRoleThan(Roles.Member)
*/
hasALowerOrEqualTeamRoleThan (role) {
if (this.isVisitingAdmin) {
return true
}

return role <= this.teamMembership?.role
const { hasALowerOrEqualTeamRoleThan } = usePermissions()
return hasALowerOrEqualTeamRoleThan(role, this.teamMembership)
}
}
}
2 changes: 1 addition & 1 deletion frontend/src/pages/instance/Settings/Security.vue
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default {
mounted () {
this.checkAccess()
this.getSettings()
if (this.isHTTPBearerTokensFeatureEnabledForTeam()) {
if (this.isHTTPBearerTokensFeatureEnabled()) {
this.getTokens()
}
},
Expand Down
58 changes: 57 additions & 1 deletion frontend/src/store/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,63 @@ const getters = {

teamInvitations: state => state.invitations,
teamInvitationsCount: state => state.invitations?.length || 0,
hasAvailableTeams: state => state.teams.length > 0
hasAvailableTeams: state => state.teams.length > 0,

featuresCheck: (state) => {
const preCheck = {
// Shared Library
isSharedLibraryFeatureEnabledForTeam: ((state) => {
const flag = state.team?.type?.properties?.features?.['shared-library']
return flag === undefined || flag
})(state),
isSharedLibraryFeatureEnabledForPlatform: state.features['shared-library'],

// Blueprints
isBlueprintsFeatureEnabledForTeam: ((state) => {
const flag = state.team?.type?.properties?.features?.flowBlueprints
return flag === undefined || flag
})(state),
isBlueprintsFeatureEnabledForPlatform: !!state.features.flowBlueprints,

// Custom Catalogs
isCustomCatalogsFeatureEnabledForPlatform: !!state.features.customCatalogs,
isCustomCatalogsFeatureEnabledForTeam: ((state) => {
const flag = state.team.type.properties.features?.customCatalogs
return flag === undefined || flag
})(state),

// Static Assets
isStaticAssetFeatureEnabledForPlatform: !!state.features.staticAssets,
isStaticAssetsFeatureEnabledForTeam: !!state.team?.type?.properties?.features?.staticAssets,

// HTTP BearerTokens
isHTTPBearerTokensFeatureEnabledForPlatform: !!state.settings?.features.httpBearerTokens,
isHTTPBearerTokensFeatureEnabledForTeam: !!state.team?.type.properties.features.teamHttpSecurity,

// BOM
isBOMFeatureEnabledForPlatform: !!state.features.bom,
isBOMFeatureEnabledForTeam: !!state.team?.type?.properties?.features?.bom,

// Timeline
isTimelineFeatureEnabledForPlatform: !!state.features.projectHistory,
isTimelineFeatureEnabledForTeam: !!state.team?.type?.properties?.features?.projectHistory,

// Mqtt Broker
isMqttBrokerFeatureEnabledForPlatform: !!state.features.teamBroker,
isMqttBrokerFeatureEnabledForTeam: !!state.team?.type?.properties?.features?.teamBroker
}
return {
...preCheck,
isSharedLibraryFeatureEnabled: preCheck.isSharedLibraryFeatureEnabledForTeam && preCheck.isSharedLibraryFeatureEnabledForPlatform,
isBlueprintsFeatureEnabled: preCheck.isBlueprintsFeatureEnabledForTeam && preCheck.isBlueprintsFeatureEnabledForPlatform,
isCustomCatalogsFeatureEnabled: preCheck.isCustomCatalogsFeatureEnabledForPlatform && preCheck.isCustomCatalogsFeatureEnabledForTeam,
isStaticAssetFeatureEnabled: preCheck.isStaticAssetFeatureEnabledForPlatform && preCheck.isStaticAssetsFeatureEnabledForTeam,
isHTTPBearerTokensFeatureEnabled: preCheck.isHTTPBearerTokensFeatureEnabledForPlatform && preCheck.isHTTPBearerTokensFeatureEnabledForTeam,
isBOMFeatureEnabled: preCheck.isBOMFeatureEnabledForPlatform && preCheck.isBOMFeatureEnabledForTeam,
isTimelineFeatureEnabled: preCheck.isTimelineFeatureEnabledForPlatform && preCheck.isTimelineFeatureEnabledForTeam,
isMqttBrokerFeatureEnabled: preCheck.isMqttBrokerFeatureEnabledForPlatform && preCheck.isMqttBrokerFeatureEnabledForTeam
}
}
}

const mutations = {
Expand Down
Loading