Skip to content
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
4 changes: 2 additions & 2 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "client",
"version": "0.9.0",
"version": "0.10.0",
"private": true,
"scripts": {
"build": "vite build",
Expand Down
19 changes: 5 additions & 14 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
</b-navbar-nav>
</b-collapse>
</b-navbar>
<template>

Check warning on line 105 in client/src/App.vue

View workflow job for this annotation

GitHub Actions / build

`<template>` require directive
<div
v-if="!loaded"
class="text-center center-spinner"
Expand Down Expand Up @@ -155,9 +155,13 @@
startingSession: false,
};
},
async created() {
await this.GET_SETTINGS();
await this.awaitWSConnect();
},
methods: {
...mapActions(['GET_SHOW_SESSION_DATA', 'GET_CURRENT_USER', 'USER_LOGOUT', 'GET_RBAC_ROLES',
'GET_CURRENT_RBAC']),
'GET_CURRENT_RBAC', 'GET_SETTINGS']),
async awaitWSConnect() {
if (this.WEBSOCKET_HEALTHY) {
clearTimeout(this.loadTimer);
Expand Down Expand Up @@ -237,7 +241,7 @@
}
},
},
computed: {

Check warning on line 244 in client/src/App.vue

View workflow job for this annotation

GitHub Actions / build

The "computed" property should be above the "created" property on line 158
isAdminUser() {
return this.CURRENT_USER != null && this.CURRENT_USER.is_admin;
},
Expand Down Expand Up @@ -266,19 +270,6 @@
...mapGetters(['WEBSOCKET_HEALTHY', 'CURRENT_SHOW_SESSION', 'SETTINGS', 'CURRENT_USER',
'RBAC_ROLES', 'CURRENT_USER_RBAC', 'INTERNAL_UUID']),
},
async created() {
this.$router.beforeEach(async (to, from, next) => {
if (!this.SETTINGS.has_admin_user) {
this.$toast.error('Please create an admin user before continuing');
next(false);
} else if (to.fullPath === '/config' && (this.CURRENT_USER == null || !this.CURRENT_USER.is_admin)) {
next(false);
} else {
next();
}
});
await this.awaitWSConnect();
},
};
</script>

Expand Down
116 changes: 116 additions & 0 deletions client/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,92 +9,110 @@ const routes = [
path: '/',
name: 'home',
component: HomeView,
meta: { requiresAuth: false },
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
meta: { requiresAuth: false },
},
{
path: '/config',
name: 'config',
component: () => import('../views/config/ConfigView.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
},
{
path: '/show-config',
component: () => import('../views/show/ShowConfigView.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
children: [
{
name: 'show-config',
path: '',
component: () => import('../views/show/config/ConfigShow.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-cast',
path: 'cast',
component: () => import('../views/show/config/ConfigCast.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-characters',
path: 'characters',
component: () => import('../views/show/config/ConfigCharacters.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-character-groups',
path: 'character-groups',
component: () => import('../views/show/config/ConfigCharacterGroups.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-acts',
path: 'acts',
component: () => import('../views/show/config/ConfigActs.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-scenes',
path: 'scenes',
component: () => import('../views/show/config/ConfigScenes.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-cues',
path: 'cues',
component: () => import('../views/show/config/ConfigCues.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-mics',
path: 'mics',
component: () => import('../views/show/config/ConfigMics.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-config-script',
path: 'script',
component: () => import('../views/show/config/ConfigScript.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
{
name: 'show-sessions',
path: 'sessions',
component: () => import('../views/show/config/ConfigSessions.vue'),
meta: { requiresAuth: true, requiresShowAccess: true },
},
],
},
{
path: '/live',
name: 'live',
component: () => import('../views/show/ShowLiveView.vue'),
meta: { requiresAuth: false },
},
{
path: '/login',
name: 'login',
component: () => import('../views/user/LoginView.vue'),
meta: { requiresAuth: false },
},
{
path: '/me',
name: 'user_settings',
component: () => import('../views/user/Settings.vue'),
meta: { requiresAuth: true },
},
{
path: '*',
name: '404',
component: () => import('../views/404View.vue'),
meta: { requiresAuth: false },
},
];

Expand All @@ -104,4 +122,102 @@ const router = new VueRouter({
routes,
});

router.beforeEach(async (to, from, next) => {
// Silly code needed for... reasons... this seems to correctly set the correct state
// on the router and do the fetch needed for anything else. Shrug...
let requiresSettingsFetch = false;
if (router.app.$store === undefined) {
await router.app.$nextTick();
requiresSettingsFetch = true;
}
if (requiresSettingsFetch) {
await router.app.$store.dispatch('GET_SETTINGS');
await router.app.$store.dispatch('GET_CURRENT_USER');
if (router.app.$store.getters.CURRENT_USER) {
await router.app.$store.dispatch('GET_CURRENT_RBAC');
}
}

const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
const requiresAdmin = to.matched.some((record) => record.meta.requiresAdmin);
const requiresShowAccess = to.matched.some((record) => record.meta.requiresShowAccess);

// Check if the system has admin user set up
if (router.app.$store.getters.SETTINGS && !router.app.$store.getters.SETTINGS.has_admin_user) {
if (to.path !== '/') {
Vue.$toast.error('Please create an admin user before continuing');
return next('/');
}
return next();
}

const currentUser = router.app.$store.getters.CURRENT_USER;
const isAuthenticated = currentUser !== null;
const isAdmin = currentUser?.is_admin === true;

// Helper function to check show access
const hasShowAccess = () => {
if (!router.app.$store.getters.CURRENT_SHOW) return false;

// Admin always has access
if (isAdmin) return true;

// Check RBAC permissions for show edit/execute
const rbacRoles = router.app.$store.getters.RBAC_ROLES;
const userRbac = router.app.$store.getters.CURRENT_USER_RBAC;

if (!rbacRoles.length || !userRbac || !Object.keys(userRbac).includes('shows')) {
return false;
}

const writeMask = rbacRoles.find((x) => x.key === 'WRITE')?.value || 0;
const executeMask = rbacRoles.find((x) => x.key === 'EXECUTE')?.value || 0;

// Bitwise check if user has WRITE or EXECUTE permission for shows
return userRbac.shows && userRbac.shows[0]
// eslint-disable-next-line no-bitwise
&& ((userRbac.shows[0][1] & (writeMask | executeMask)) !== 0);
};

// Check authentication requirements
if (requiresAuth && !isAuthenticated) {
Vue.$toast.error('Please log in to access this page');
return next('/login');
}

// Check admin requirements
if (requiresAdmin && !isAdmin) {
Vue.$toast.error('Admin access required');
return next('/');
}

// Check show access requirements
if (requiresShowAccess) {
// First check if there's a current show
if (!router.app.$store.getters.CURRENT_SHOW) {
Vue.$toast.error('No show is currently selected');
return next('/');
}

// Then check permissions
if (!hasShowAccess()) {
Vue.$toast.error('You do not have permission to access show configuration');
return next('/');
}
}

// Handle special case for live route
if (to.path === '/live') {
await router.app.$store.dispatch('GET_SHOW_SESSION_DATA');
const showSession = router.app.$store.getters.CURRENT_SHOW_SESSION;
const websocketHealthy = router.app.$store.getters.WEBSOCKET_HEALTHY;

if (!showSession || !websocketHealthy) {
Vue.$toast.error('No active show session or connection issue');
return next('/');
}
}
return next();
});

export default router;
8 changes: 6 additions & 2 deletions client/src/store/modules/user/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import log from 'loglevel';

import { makeURL } from '@/js/utils';
import router from '@/router';
import { isEmpty } from 'lodash';
import settings from './settings';

export default {
Expand Down Expand Up @@ -94,7 +95,9 @@ export default {
if (response.ok) {
await context.commit('SET_CURRENT_USER', null);
Vue.$toast.success('Successfully logged out!');
router.push('/');
if (router.currentRoute.path !== '/') {
router.push('/');
}
} else {
log.error('Unable to log out');
Vue.$toast.error('Unable to log out!');
Expand All @@ -104,7 +107,8 @@ export default {
const response = await fetch(`${makeURL('/api/v1/auth')}`);
if (response.ok) {
const user = await response.json();
await context.commit('SET_CURRENT_USER', user);
const userJson = isEmpty(user) ? null : user;
await context.commit('SET_CURRENT_USER', userJson);
} else {
log.error('Unable to get current user');
}
Expand Down
Loading
Loading