CASL - Vue 3 - Element not showing for role - vue.js

I am having a bit of a challenge implementing CASL in my app.
I have created the following composable useAppAbility ("hook") that defines all the rules:
import { AbilityBuilder, createMongoAbility, subject } from "#casl/ability";
import { useAbility } from "#casl/vue";
const service = {};
const user = {};
const subscription = {};
const invoice = {};
const account = {};
const ability = createMongoAbility();
const ROLES = ["admin", "account_owner", "beneficiary", "super_admin"];
const defineAbilityFor = (role: Object) => {
const { can, rules } = new AbilityBuilder(createMongoAbility);
const is = (r: string) => {
return ROLES.indexOf(r) >= ROLES.indexOf(role);
};
if (is("admin")) {
can("add", subject("User", user));
can("remove", subject("User", user));
}
return ability.update(rules);
};
export { defineAbilityFor, ability, subject };
export const useAppAbility = () => useAbility();
Added the plugin to the main.ts:
import { ability } from "#/composables/useAppAbility";
import { abilitiesPlugin } from "#casl/vue";
createApp(App)
.use(abilitiesPlugin, ability, {
useGlobalProperties: true,
})
//stuff
.mount("#app");
And then, I found that using the beforeEach hook in the router and passing in the user before each route was the simplest way to deal with page load and SPA routing.
I have therefore added the following to my router/index.ts:
import { ability, defineAbilityFor } from "#/composables/useAppAbility";
import useAuth from "#/composables/useAuth";
const {
getUserByClaims,
} = useAuth();
// routes
router.beforeEach(async (to, _from, next) => {
defineAbilityFor(getUserByClaims.value.roles)
})
At this stage I can verify that the user is being passed properly to the defineAbilityFor function and when using the ability.on("update") hook to log the rules object, I have the following output:
Which seems to confirm that the rules for this user are built and updated correctly?
However, when trying to display a button for the said admin in a component, the button does not show.
MyComponent.vue:
<script setup lang="ts">
import { useAppAbility, subject } from "#/composables/useAppAbility";
const { can } = useAppAbility();
</script>
<template>
<div v-if="can('add', subject('User', {}))">TEST FOR CASL</div> <!-- DOES NOT SHOW-->
</template>
Not sure where to go from there, any help would be appreciated.
Thanks

Related

Why is 'route.params.id' unavailable/undefined in setup method?

I'm working with vue-router and Vue 3. I have a view where I'd like to take the router url and use it to call a method to access an API. This method returns a promise I can use to populate my page. When calling my method with 'route.params.id', it says that the parameter is undefined. When I do console.log(route.params.id), it displays correctly in console. I've tried using a computed property instead, but I had the same issue.
Setup code:
import { ref } from "vue";
import MovieApiService from "../api/MovieApiService";
import { useRoute } from "vue-router";
export default {
setup() {
const movie = ref([]);
const route = useRoute();
MovieApiService.getMovie(route.params.id).then((response) => {
movie.value = response.data.results;
});
return {
movie,
};
},
method: {},
};
Method being called:
static getMovie(body: GetMovieByTmdbId) {
return axios.get(
`https://api.themoviedb.org/3/movie/${body.id}?api_key=${apiKey}`
);
}
Here's what I tried to compute the property instead, with the same result.
setup() {
const route = useRoute();
const id = computed(()=>{return route.params.id})
const movie = ref([]);
getMovie(id).then((response) => {
movie.value = response.data.results;
console.log(movie.value);
});
How should I ensure this value is available when I call my method?
You are giving route.params.id as a variable to getMovie(body).
You are then using body.id in your URL. This would equal route.params.id.id which is not defined.
Use body in your URL, or change the parameter to id so is makes more sense.
Like this:
...
const route = useRoute();
MovieApiService.getMovie(route.params.id).then((response) => {
movie.value = response.data.results;
});
...
const getMovie = (id) => {
return axios.get(
`https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}`
);
}

Cannot use Vue-Router to get the parameters in the URL

Today, when trying to use Vue-Router (in Vue-CLI) to get URL parameters, I encountered difficulties ($route.query is empty), the code is as follows.
Code purpose: Get the parameters carried after the URL (such as client_id in "http://localhost:8080/#/?client_id=00000000000077")
Project file structure:
router/index.js:
App.vue(Get part of the code for URL parameters):
The running result of this part of the code:
I'm not sure why $router.currentRoute and $route aren't matching up, but you could simply use $router.currentRoute.query.client_id if you need it in mounted().
Another workaround is to use a $watch on $route.query.client_id:
export default {
mounted() {
const unwatch = this.$watch('$route.query.client_id', clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}
Or watch in the Composition API:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
export default {
mounted() {
console.log({
route: this.$route,
router: this.$router,
})
},
setup() {
const route = useRoute()
const unwatch = watch(() => route.query.client_id, clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}

How to dynamically access a remote component in vue js with module federation

I am trying to build a vue js 2 microfrontend with module federation. I dont want to use static remote imports via the webpack.config.js like this
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1#http://localhost:3001/remoteEntry.js',
},
}),
],
};
I am looking for a way to dynamically import vue components into my host application. I tried this approach so far, but i only found examples that worked with angular or react.
The goal is to have multiple remote frontends that can automatically register somewhere, maybe in some kind of store. The host application then can access this store and get all of the registered remote applications (name, url, components). The host application then loads the components and should be able to use them. I remote import the component HelloDerp, the loading process is working fine but i dont know how to render it on my host application. I read the vue js doc about dynamic and async imports but i think that only works for local components.
What i've got so far in the host application:
<template>
<div id="app">
<HelloWorld />
<HelloDerp />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
const HelloDerp = null;
export default {
name: "App",
components: {
HelloWorld,
HelloDerp,
},
mounted() {
var remoteUrlWithVersion = "http://localhost:9000/remoteEntry.js";
const element = document.createElement("script");
element.type = "text/javascript";
element.async = true;
element.src = remoteUrlWithVersion;
element.onload = () => {
console.log(`Dynamic Script Loaded: ${element.src}`);
HelloDerp = loadComponent("core", "./HelloDerp");
};
document.head.appendChild(element);
return null;
},
};
async function loadComponent(scope, module) {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
const container = window[scope]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
}
</script>
Sorry i almost forgot about this. Here's my solution.
Load Modules:
export default async function loadModules(
host: string,
ownModuleName: string,
wantedNames: string[]
): Promise<RemoteComponent[]> {
...
uiApplications.forEach((uiApplication) => {
const remoteURL = `${uiApplication.protocol}://${uiApplication.host}:${uiApplication.port}/${uiApplication.moduleName}/${uiApplication.fileName}`;
const { componentNames } = uiApplication;
const { moduleName } = uiApplication;
const element = document.createElement('script');
element.type = 'text/javascript';
element.async = true;
element.src = remoteURL;
element.onload = () => {
componentNames?.forEach((componentName) => {
const component = loadModule(moduleName, `./${componentName}`);
component.then((result) => {
if (componentName.toLowerCase().endsWith('view')) {
// share views
components.push(new RemoteComponent(result.default, componentName));
} else {
// share business logic
components.push(new RemoteComponent(result, componentName));
}
});
});
};
document.head.appendChild(element);
});
});
...
}
export default async function loadModule(scope: string, module: string): Promise<any> {
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
}
Add Modules to routes
router.addRoute({
name: remoteComponent.componentName,
path: `/${remoteComponent.componentName}`,
component: remoteComponent.component,
});

Vue Composition API reactivity doesn't work properly

I am using Vue2, Vuetify, Vue Composition API(#vue/composition-api)
The problem I faced is that composition api reactivity doesn't work properly.
Let me show you some code
---- companies.vue ----
<template>
...
<v-data-table
:headers="companiesHeaders"
:items="companies"
:loading="loadingCompanies"
/>
...
</template>
<script>
...
import { useCompanies } from '#/use/companies'
export default {
setup: (_, props) => {
...
const {
companies,
loadingCompanies,
getCompanies
} = useCompanies(context)
onMounted(getCompanies)
return {
...,
companies,
loadingCompanies
}
}
}
</script>
---- #/use/companies.ts ----
import { ref } from '#vue/composition-api'
export const useCompanies = (context: any) => {
const { emit, root } = context
const companies = ref([])
const loadingCompanies = ref(false)
const getCompanies = async () => {
if (loadingCompanies.value) { return }
try {
loadingCompanies.value = true
companies.value = (await root.$repositories
.companies.getCompanies()).data
console.log(companies.value)
// This log works properly. It logs company list once received
// But even after this async function is finished, companies and loadingCompanies are not updated automatically
} catch (err) {} finally {
loadingCompanies.value = false
}
}
return {
companies,
loadingCompanies
}
}
I tried with both ref and reactive.
But reactivity for whatever inside companies.vue doesn't work.
I resolved the issue.
The issue was that company variable instance was created in 2 places(one for create company dialog and one for table), so changes in one place(create company dialog) didn't affect to the other(table).
Thanks.

Vuex Getters Come Back as Undefined

I'm having a Vuex getters issue where the gitters return as undefined (in the Vue Dev Console and no errors are logged in the Chrome Dev Console).
If mapGetters() is commented out (like the example code below), the returned data is displayed on screen -> Providing if the user clicks into the link that has the data. The data will NOT display if the user enters the app directly at the point where the data should display.
There is a similar question but there is no accepted answer
Vue Console Logs:
STATE:
$_lgdHRS:Object
totHrs:129
GETTERS:
$_lgdHRS/totHrs:undefined
SomeContainer.vue
<script>
import store from '../../_store'
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState('$_lgdHRS',{
totHrs : 'totHrs',
}),
// ...mapGetters('$_lgdHRS',{
// totHrs : 'totHrs',
// airHrs : 'airHrs',
// picHrs : 'picHrs',
// pmcHrs : 'pmcHrs',
// voHrs : 'voHrs',
// trngHrs : 'trngHrs'
// }),
},
created() {
this.storeKey = '$_lgdHRS';
if (!(this.storeKey in this.$store._modules.root._children)) {
this.$store.registerModule(this.storeKey, store);
}
},
mounted() {
this.$store.dispatch('$_lgdHRS/getLogSummary');
},
}
</script>
<template>
<total-summary :hours="totHrs" />
</template>
state.js
export const state = {
totHrs: Number,
}
getters.js
const totHrs = state => state.totHrs;
export default {
totHrs,
};
mutations.js
const
TOTAL_HRS_UPDATED = (state, totHrs) => {
state.totHrs = +totHrs;
};
export default {
TOTAL_HRS_UPDATED,
};
Most probably because you have just displatched the request in mounted and before the data is set into the state variable your component is displayed.
Hence you can trying using async await in mounted as well as in store actions.
Do refer the following link and check the last example in this.
https://vuex.vuejs.org/guide/actions.html
The problem was that I was nesting my variables as I usually would in other frameworks.
Example:
// NESTED VARS
const r = response
totHrs = r.total,
airHrs = r.airborne,
picHrs = r.PIC,
pmcHrs = r.PMC,
voHrs = r.VO,
trngHrs = r.training;
// CHANGE TO:
const r = response
const totHrs = r.total
const airHrs = r.airborne
const picHrs = r.PIC
const pmcHrs = r.PMC
const voHrs = r.VO
const trngHrs = r.training
I don't know enough to why but your input would be greatly appreciated in the comments.