Skip to content

Commit

Permalink
feat(dashboard): make guard for router, insted of using if conditions…
Browse files Browse the repository at this point in the history
… on each page
  • Loading branch information
Satont committed Jul 28, 2023
1 parent e76e1bd commit fa88a63
Show file tree
Hide file tree
Showing 17 changed files with 458 additions and 369 deletions.
28 changes: 24 additions & 4 deletions frontend/dashboard/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query';
import { Profile } from '@twir/grpc/generated/api/api/auth';
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
import { Dashboard, Profile } from '@twir/grpc/generated/api/api/auth';
import { computed } from 'vue';

import { protectedApiClient } from './twirp.js';
Expand All @@ -26,9 +26,12 @@ export const useLogout = () => useMutation({
},
});

export const getProfile = async () => {
export const getProfile = async (queryClient: QueryClient) => {
const call = await protectedApiClient.authUserProfile({});
return call.response;
queryClient.setQueryData(profileQueryKey, call.response);

const dashboardsCall = await protectedApiClient.authGetDashboards({});
queryClient.setQueryData(dashboardsQueryKey, dashboardsCall.response);
};

const dashboardsQueryKey = ['authDashboards'];
Expand Down Expand Up @@ -92,6 +95,9 @@ export const PERMISSIONS_FLAGS = {

VIEW_ROLES: 'Can view roles',
MANAGE_ROLES: 'Can manage roles',

VIEW_EVENTS: 'Can view events',
MANAGE_EVENTS: 'Can manage events',
};

export type PermissionsType = keyof typeof PERMISSIONS_FLAGS
Expand All @@ -114,3 +120,17 @@ export const useUserAccessFlagChecker = (flag: PermissionsType) => {
return dashboard.flags.includes(flag);
});
};

export const userAccessFlagChecker = async (queryClient: QueryClient, flag: PermissionsType) => {
const profile = await queryClient.getQueryData(profileQueryKey) as Profile | null;
const { dashboards } = await queryClient.getQueryData(dashboardsQueryKey) as { dashboards: Dashboard[] };

if (!dashboards || !profile || !profile.selectedDashboardId) return false;
if (profile.selectedDashboardId == profile.id) return true;

const dashboard = dashboards.find(d => d.id === profile.selectedDashboardId);
if (!dashboard) return false;

if (dashboard.flags.includes('CAN_ACCESS_DASHBOARD')) return true;
return dashboard.flags.includes(flag);
};
9 changes: 6 additions & 3 deletions frontend/dashboard/src/components/events/card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useI18n } from 'vue-i18n';
import { getOperation, flatEvents } from './helpers.js';
import { EditableEvent } from './types.js';
import { useEventsManager } from '@/api/index.js';
import { useEventsManager, useUserAccessFlagChecker } from '@/api/index.js';
import Card from '@/components/card/card.vue';
const props = defineProps<{
Expand All @@ -29,13 +29,16 @@ const throttledSwitchState = useThrottleFn((v: boolean) => {
}, 500);
const { t } = useI18n();
const userCanEditEvents = useUserAccessFlagChecker('MANAGE_EVENTS');
</script>

<template>
<card style="height:100%" :icon="flatEvents[event.type]?.icon" :title="getEventName(event.type)">
<template #headerExtra>
<n-switch
:value="event.enabled"
:disabled="!userCanEditEvents"
@update-value="(v) => throttledSwitchState(v)"
/>
</template>
Expand Down Expand Up @@ -64,7 +67,7 @@ const { t } = useI18n();
</template>
<template #footer>
<n-button secondary size="large" @click="$emit('openSettings', event.id)">
<n-button secondary size="large" :disabled="!userCanEditEvents" @click="$emit('openSettings', event.id)">
<span>{{ t('sharedButtons.settings') }}</span>
<IconSettings />
</n-button>
Expand All @@ -74,7 +77,7 @@ const { t } = useI18n();
@positive-click="eventsDeleter.mutateAsync({ id: event.id! })"
>
<template #trigger>
<n-button secondary type="error" size="large">
<n-button secondary type="error" size="large" :disabled="!userCanEditEvents">
<span>{{ t('sharedButtons.delete') }}</span>
</n-button>
</template>
Expand Down
1 change: 0 additions & 1 deletion frontend/dashboard/src/components/events/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,5 @@ export const flatEvents = createFlat(EVENTS);
export const flatOperations = createFlat(OPERATIONS);

export const getOperation = (type: string): Operation | undefined => {
console.log(flatOperations);
return flatOperations[type];
};
229 changes: 135 additions & 94 deletions frontend/dashboard/src/layout/sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,114 +36,155 @@ import { RouterLink, useRouter } from 'vue-router';
import DashboardMenu from './dashboardsMenu.vue';
import { renderIcon } from '../helpers/index.js';
import { useProfile, useTwitchGetUsers } from '@/api/index.js';
import { useProfile, useTwitchGetUsers, useUserAccessFlagChecker } from '@/api/index.js';
defineProps<{
isCollapsed: boolean
}>();
const router = useRouter();
const activeKey = ref<string | null>('/');
const menuOptions: (MenuOption | MenuDividerOption)[] = [
{
label: 'Dashboard',
icon: renderIcon(IconDashboard),
path: '/dashboard',
},
{
label: 'Integrations',
icon: renderIcon(IconBox),
path: '/dashboard/integrations',
},
{
label: 'Events',
icon: renderIcon(IconCalendarEvent),
path: '/dashboard/events',
},
{
label: 'OBS Overlays',
icon: renderIcon(IconDeviceDesktop),
path: '/dashboard/overlays',
},
{
label: 'Song Requests',
icon: renderIcon(IconHeadphones),
path: '/dashboard/song-requests',
},
{
label: 'Commands',
icon: renderIcon(IconCommand),
children: [
{ label: 'Custom', icon: renderIcon(IconPencilPlus), path: '/dashboard/commands/custom' },
{ label: 'Stats', icon: renderIcon(IconDeviceDesktopAnalytics), path: '/dashboard/commands/stats' },
{ label: 'Moderation', icon: renderIcon(IconSword), path: '/dashboard/commands/moderation' },
{ label: 'Songs', icon: renderIcon(IconPlaylist), path: '/dashboard/commands/songs' },
{ label: 'Manage', icon: renderIcon(IconClipboardCopy), path: '/dashboard/commands/manage' },
],
},
{
label: 'Users',
icon: renderIcon(IconUsers),
path: '/dashboard/community/users',
},
{
label: 'Permissions',
icon: renderIcon(IconShieldHalfFilled),
path: '/dashboard/community/roles',
},
{
label: 'Timers',
icon: renderIcon(IconClockHour7),
path: '/dashboard/timers',
},
// {
// label: 'Moderation',
// icon: renderIcon(IconSword),
// path: '/dashboard/moderation'
// },
{
label: 'Keywords',
icon: renderIcon(IconKey),
path: '/dashboard/keywords' },
{
label: 'Variables',
icon: renderIcon(IconActivity),
path: '/dashboard/variables' },
{
label: 'Greetings',
icon: renderIcon(IconSpeakerphone),
path: '/dashboard/greetings',
},
{
type: 'divider',
},
].map((item) => ({
...item,
key: item.path ?? item.label,
label: !item.path ? item.label ?? undefined : () => h(
RouterLink,
const menuOptions = computed<(MenuOption | MenuDividerOption)[]>(() => {
const canViewIntegrations = useUserAccessFlagChecker('VIEW_INTEGRATIONS');
const canViewEvents = useUserAccessFlagChecker('VIEW_EVENTS');
const canViewOverlays = useUserAccessFlagChecker('VIEW_OVERLAYS');
const canViewSongRequests = useUserAccessFlagChecker('VIEW_SONG_REQUESTS');
const canViewCommands = useUserAccessFlagChecker('VIEW_COMMANDS');
const canViewTimers = useUserAccessFlagChecker('VIEW_TIMERS');
const canViewKeywords = useUserAccessFlagChecker('VIEW_KEYWORDS');
const canViewVariabls = useUserAccessFlagChecker('VIEW_VARIABLES');
const canViewGreetings = useUserAccessFlagChecker('VIEW_GREETINGS');
const canViewRoles = useUserAccessFlagChecker('VIEW_ROLES');
return [
{
to: {
path: item.path,
},
label: 'Dashboard',
icon: renderIcon(IconDashboard),
path: '/dashboard',
},
{
label: 'Integrations',
icon: renderIcon(IconBox),
path: '/dashboard/integrations',
disabled: !canViewIntegrations.value,
},
{
label: 'Events',
icon: renderIcon(IconCalendarEvent),
path: '/dashboard/events',
disabled: !canViewEvents.value,
},
{
label: 'OBS Overlays',
icon: renderIcon(IconDeviceDesktop),
path: '/dashboard/overlays',
disabled: !canViewOverlays.value,
},
{
label: 'Song Requests',
icon: renderIcon(IconHeadphones),
path: '/dashboard/song-requests',
disabled: !canViewSongRequests.value,
},
{
label: 'Commands',
icon: renderIcon(IconCommand),
disabled: !canViewCommands.value,
children: [
{
label: 'Custom',
icon: renderIcon(IconPencilPlus),
path: '/dashboard/commands/custom',
},
{
label: 'Stats',
icon: renderIcon(IconDeviceDesktopAnalytics),
path: '/dashboard/commands/stats',
},
{
label: 'Moderation',
icon: renderIcon(IconSword),
path: '/dashboard/commands/moderation',
},
{
label: 'Songs',
icon: renderIcon(IconPlaylist),
path: '/dashboard/commands/songs',
},
{
label: 'Manage',
icon: renderIcon(IconClipboardCopy),
path: '/dashboard/commands/manage',
},
],
},
{ default: () => item.label },
),
children: item.children?.map((child) => ({
...child,
key: child.path,
label: () => h(
{
label: 'Users',
icon: renderIcon(IconUsers),
path: '/dashboard/community/users',
},
{
label: 'Permissions',
icon: renderIcon(IconShieldHalfFilled),
path: '/dashboard/community/roles',
disabled: !canViewRoles.value,
},
{
label: 'Timers',
icon: renderIcon(IconClockHour7),
path: '/dashboard/timers',
disabled: !canViewTimers.value,
},
{
label: 'Keywords',
icon: renderIcon(IconKey),
path: '/dashboard/keywords',
disabled: !canViewKeywords.value,
},
{
label: 'Variables',
icon: renderIcon(IconActivity),
path: '/dashboard/variables',
disabled: !canViewVariabls.value,
},
{
label: 'Greetings',
icon: renderIcon(IconSpeakerphone),
path: '/dashboard/greetings',
disabled: !canViewGreetings.value,
},
{
type: 'divider',
},
].map((item) => ({
...item,
key: item.path ?? item.label,
extra: item.disabled ? 'No perms' : undefined,
label: !item.path || item.disabled ? item.label ?? undefined : () => h(
RouterLink,
{
to: {
path: child.path,
path: item.path,
},
},
{ default: () => child.label },
{ default: () => item.label },
),
})),
}));
const router = useRouter();
children: item.children?.map((child) => ({
...child,
key: child.path,
label: item.disabled ? child.label : () => h(
RouterLink,
{
to: {
path: child.path,
},
},
{ default: () => child.label },
),
})),
}));
});
onMounted(async () => {
await router.isReady();
Expand Down
Loading

0 comments on commit fa88a63

Please sign in to comment.