Skip to content
This repository has been archived by the owner on Apr 3, 2024. It is now read-only.

Commit

Permalink
Add CASL to frontend, conditional insufficient permissions pages
Browse files Browse the repository at this point in the history
  • Loading branch information
robere2 committed Jul 23, 2022
1 parent 954e9cb commit ced5e8c
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 40 deletions.
32 changes: 20 additions & 12 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@
<n-config-provider :theme="darkTheme" :theme-overrides="theme">
<n-loading-bar-provider>
<n-message-provider placement="bottom">
<Page />
<Suspense>
<Page/>
<template #fallback>
<div class="glimpse-loading">
<n-spin></n-spin>
<p>Loading...</p>
</div>
</template>
</Suspense>
</n-message-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import {defineComponent} from "vue";
import Page from "./Page.vue";
import { NMessageProvider, NConfigProvider, NLoadingBarProvider, darkTheme } from "naive-ui";
import {useAuthStore} from "@/stores/auth";
import {NMessageProvider, NConfigProvider, NLoadingBarProvider, darkTheme, NSpin} from "naive-ui";
export default defineComponent({
name: "App",
async mounted() {
const authStore = useAuthStore();
const ownId = await authStore.getOwnId();
if(typeof ownId === "number") {
authStore.isLoggedIn = true;
}
await authStore.getPermissions();
},
data: () => {
return {
layoutCssName: "wave-layout",
Expand Down Expand Up @@ -58,11 +57,20 @@ export default defineComponent({
NMessageProvider,
NConfigProvider,
NLoadingBarProvider,
NSpin,
Page
}
});
</script>

<style lang="scss">
@import "./assets/base.css";
.glimpse-loading {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
}
</style>
9 changes: 9 additions & 0 deletions src/Page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,18 @@ import Footer from "@/components/Footer.vue";
import { onMounted, onUnmounted, ref, watch } from "vue";
import { useLoadingBar, useMessage } from "naive-ui";
import { useRoute } from "vue-router";
import {useAuthStore} from "@/stores/auth";
const route = useRoute();
const message = useMessage();
const authStore = useAuthStore();
// Fetch identity and permissions from server
const ownId = await authStore.getOwnId();
if(typeof ownId === "number") {
authStore.isLoggedIn = true;
}
await authStore.getPermissions();
// Listen for scrolling to update the logo size
const scrollY = ref(window.scrollY);
Expand Down
33 changes: 33 additions & 0 deletions src/casl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Ability } from "@casl/ability";
import type { AbilityClass } from "@casl/ability";
import { ABILITY_TOKEN, useAbility } from "@casl/vue";
import type { InjectionKey, Ref } from "vue";
import { computed } from "vue";

export type AbilityActions = "read" | "create" | "update" | "delete" | "manage";
export type AbilitySubjects =
| "Production"
| "User"
| "Role"
| "ContactSubmission";
export type GlimpseAbility = Ability<[AbilityActions, AbilitySubjects]>;

export const GlimpseAbility = Ability as AbilityClass<GlimpseAbility>;
export const TOKEN = ABILITY_TOKEN as InjectionKey<GlimpseAbility>;
export const ability = new GlimpseAbility();

export function useGlimpseAbility() {
return useAbility<GlimpseAbility>();
}

export function requirePermission(
action: AbilityActions,
subject: AbilitySubjects,
field?: string
): () => Ref<boolean> {
return () => {
return computed(() => {
return ability.can(action, subject, field);
});
};
}
2 changes: 1 addition & 1 deletion src/components/StaticBackground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ if(mediaQuery && mediaQuery.matches) {
onMounted(() => {
interval = setInterval(() => {
generateNoise();
}, 1000 / 30);
}, 1000 / 20);
})
onBeforeUnmount(() => {
if(!interval) {
Expand Down
9 changes: 8 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
faPeopleGroup,
faXmark,
} from "@fortawesome/pro-light-svg-icons";
import { faHexagonExclamation } from "@fortawesome/pro-duotone-svg-icons";
import {
faHexagonExclamation,
faDoNotEnter,
} from "@fortawesome/pro-duotone-svg-icons";
import {
faGithub,
faYoutube,
Expand All @@ -39,6 +42,7 @@ library.add(faPeopleGroup);
library.add(faXmark);

library.add(faHexagonExclamation);
library.add(faDoNotEnter);

library.add(faGithub);
library.add(faYoutube);
Expand All @@ -50,6 +54,8 @@ library.add(faRedditAlien);
import App from "./App.vue";
import router from "./router";
import { apolloClient } from "./apollo";
import { abilitiesPlugin } from "@casl/vue";
import { ability } from "@/casl";

const apolloProvider = createApolloProvider({
defaultClient: apolloClient,
Expand All @@ -62,6 +68,7 @@ const app = createApp({
render: () => h(App),
});

app.use(abilitiesPlugin, ability);
app.use(createPinia());
app.use(router);
app.use(apolloProvider);
Expand Down
77 changes: 51 additions & 26 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,36 @@ import ContactView from "../views/ContactView.vue";
import LoginView from "../views/LoginView.vue";
import DonateView from "../views/DonateView.vue";
import JoinView from "../views/JoinView.vue";
import NotFoundView from "../views/NotFoundView.vue"
import NotFoundView from "../views/NotFoundView.vue";
import NoPermissionView from "../views/NoPermissionView.vue";
import { ability } from "@/casl";
import type { AbilityActions, AbilitySubjects } from "@/casl";
import type { Component } from "vue";
import { h } from "vue";

function restrictedComponent(
component: Component,
action: AbilityActions,
subject: AbilitySubjects,
field?: string
) {
return {
functional: true,
render() {
return ability.can(action, subject, field)
? h(component)
: h(NoPermissionView);
},
};
}

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
return savedPosition;
} else {
return { top: 0, behavior: "smooth" }
return { top: 0, behavior: "smooth" };
}
},
routes: [
Expand All @@ -24,74 +45,78 @@ const router = createRouter({
name: "home",
component: HomeView,
meta: {
layoutCssName: 'wave-layout'
}
layoutCssName: "wave-layout",
},
},
{
path: "/about",
name: "about",
component: AboutView,
meta: {
layoutCssName: 'plain-layout'
}
layoutCssName: "plain-layout",
},
},
{
path: "/productions",
name: "productions",
component: ProductionsView,
component: restrictedComponent(ProductionsView, "read", "Production"),
meta: {
layoutCssName: 'plain-layout'
}
layoutCssName: "plain-layout",
},
},
{
path: "/productions",
name: "productions",
component: ProductionsView,
meta: {
layoutCssName: 'plain-layout'
}
layoutCssName: "plain-layout",
},
},
{
path: "/contact",
name: "contact",
component: ContactView,
component: restrictedComponent(
ContactView,
"create",
"ContactSubmission"
),
meta: {
layoutCssName: 'wave-layout'
}
layoutCssName: "wave-layout",
},
},
{
path: "/login",
name: "login",
component: LoginView,
meta: {
layoutCssName: 'wave-layout'
}
layoutCssName: "wave-layout",
},
},
{
path: "/donate",
name: "donate",
component: DonateView,
meta: {
layoutCssName: 'wave-layout'
layoutCssName: "wave-layout",
},
},
{
path: "/join",
name: "join",
component: JoinView,
meta: {
layoutCssName: 'wave-layout'
}
layoutCssName: "wave-layout",
},
},
{
path: '/:pathMatch(.*)*',
name: '404',
path: "/:pathMatch(.*)*",
name: "404",
component: NotFoundView,
meta: {
layoutCssName: 'plain-layout'
}
}
]
layoutCssName: "plain-layout",
},
},
],
});

export default router;
3 changes: 3 additions & 0 deletions src/stores/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { PermissionsForQuery } from "@/graphql/types";
import { provideApolloClient, useQuery } from "@vue/apollo-composable";
import { apolloClient } from "@/apollo";
import { PermissionsForDocument, SelfIdDocument } from "@/graphql/types";
import { ability } from "@/casl";

provideApolloClient(apolloClient);

Expand All @@ -29,6 +30,8 @@ export const useAuthStore = defineStore("auth", {
await new Promise<void>((resolve, reject) => {
permissionsResult.onResult(({ data }) => {
this.permissions = data.permissions;
// Cast to get rid of err about string not being covariant with AbilityActions. Assume server is correct.
ability.update(<any>this.permissions);
resolve();
});
permissionsResult.onError((error) => {
Expand Down
62 changes: 62 additions & 0 deletions src/views/NoPermissionView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<div>
<StaticBackground />
<n-result
status="403"
title="Insufficient Permissions"
description="You do not have permission to access this page."
size="huge"
>
<template #icon>
<FontAwesomeIcon icon="fa-duotone fa-do-not-enter" class="icon" />
</template>
<template #footer>
<div class="buttons-row">
<a @click="$router.back()">
<n-button>Go Back</n-button>
</a>
<RouterLink to="/">
<n-button>Go Home</n-button>
</RouterLink>
<LoginLogoutPopupButton v-if="!authStore.isLoggedIn">
<n-button>Login</n-button>
</LoginLogoutPopupButton>
</div>
</template>
</n-result>
</div>
</template>

<script setup lang="ts">
import StaticBackground from "@/components/StaticBackground.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {NResult, NButton} from "naive-ui";
import {useAuthStore} from "@/stores/auth";
import LoginLogoutPopupButton from "@/components/LoginLogoutPopupButton.vue";
const authStore = useAuthStore();
</script>

<style scoped lang="scss">
.icon {
--fa-primary-color: #ff6363;
--fa-secondary-color: #f4fbff;
--fa-secondary-opacity: 1.0;
font-size: 10em;
}
div {
padding-top: 5vw;
}
a {
text-decoration: none;
}
.buttons-row {
display: flex;
justify-content: center;
align-items: center;
* {
margin-right: 5px;
}
padding-bottom: 15px;
}
</style>

0 comments on commit ced5e8c

Please sign in to comment.