Skip to content

Commit 9442300

Browse files
committed
Create utils that allow admins to see which tutorials can access other roles of their organization
1 parent 2c08bad commit 9442300

File tree

4 files changed

+89
-68
lines changed

4 files changed

+89
-68
lines changed

frontend/src/lib/components/FlowTutorials.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<TroubleshootFlowTutorial
4141
bind:this={troubleshootFlowTutorial}
4242
index={getTutorialIndex('troubleshoot-flow')}
43-
on:error
44-
on:skipAll={skipAll}
45-
on:reload
43+
on:error
44+
on:skipAll={skipAll}
45+
on:reload
4646
/>

frontend/src/lib/tutorials/config.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ComponentType } from 'svelte'
22
import { Workflow, GraduationCap, Wrench, PlayCircle, Link2 } from 'lucide-svelte'
33
import { base } from '$lib/base'
4+
import type { Role } from './roleUtils'
45

56
export interface TutorialConfig {
67
id: string
@@ -11,14 +12,14 @@ export interface TutorialConfig {
1112
index?: number // Bitmask index in the database (for progress tracking)
1213
active?: boolean // Whether this tutorial is active and should be displayed (default: true)
1314
comingSoon?: boolean
14-
roles?: ('admin' | 'developer' | 'operator')[] // Roles that can access this tutorial (if not specified, available to everyone)
15+
roles?: Role[] // Roles that can access this tutorial (if not specified, available to everyone)
1516
order?: number
1617
}
1718

1819
export interface TabConfig {
1920
label: string
2021
tutorials: TutorialConfig[]
21-
roles?: ('admin' | 'developer' | 'operator')[] // Roles that can access this tab category (if not specified, available to everyone)
22+
roles?: Role[] // Roles that can access this tab category (if not specified, available to everyone)
2223
progressBar?: boolean // Whether to display the progress bar for this tab (default: true)
2324
active?: boolean // Whether this tab category is active and should be displayed (default: true)
2425
}
@@ -37,15 +38,12 @@ export function getTutorialIndex(id: string): number {
3738
throw new Error(`Tutorial index not found for id: ${id}. Make sure the tutorial has an index defined in config.`)
3839
}
3940

40-
// Available roles
41-
// 'developer': Developer role (can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f).
42-
// 'operator': Operator role (can execute and view scripts/flows/apps from your workspace, and only those that he has visibility on).
43-
// 'admin': Admin role (has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace).
41+
// Available roles : developer, admin, operator
4442

4543
export const TUTORIALS_CONFIG: Record<TabId, TabConfig> = {
4644
quickstart: {
4745
label: 'Quickstart',
48-
roles: ['developer', 'admin'],
46+
roles: ['admin', 'developer', 'operator'],
4947
progressBar: true,
5048
active: true,
5149
tutorials: [
@@ -60,7 +58,7 @@ export const TUTORIALS_CONFIG: Record<TabId, TabConfig> = {
6058
index: 1,
6159
active: true,
6260
comingSoon: false,
63-
roles: ['developer', 'admin'],
61+
roles: ['operator','developer', 'admin'],
6462
order: 1
6563
},
6664
{
@@ -88,7 +86,7 @@ export const TUTORIALS_CONFIG: Record<TabId, TabConfig> = {
8886
index: 3,
8987
active: true,
9088
comingSoon: false,
91-
roles: ['developer', 'admin'],
89+
roles: ['admin','developer'],
9290
order: 3
9391
}
9492
]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { UserExt } from '$lib/stores'
2+
3+
export type Role = 'admin' | 'developer' | 'operator'
4+
5+
/**
6+
* Get the effective role of a user based on their database flags.
7+
* - Admin: user.is_admin === true
8+
* - Operator: user.operator === true (and not admin)
9+
* - Developer: default (neither admin nor operator)
10+
*/
11+
export function getUserEffectiveRole(user: UserExt | null | undefined): Role | null {
12+
if (!user) return null
13+
if (user.is_admin) return 'admin'
14+
if (user.operator) return 'operator'
15+
return 'developer'
16+
}
17+
18+
/**
19+
* Check if a role has access to a required role.
20+
* This is the core role-checking logic used by both normal and preview modes.
21+
*/
22+
function checkRoleMatch(
23+
userRole: Role,
24+
requiredRole: Role
25+
): boolean {
26+
if (requiredRole === 'admin') return userRole === 'admin'
27+
if (requiredRole === 'operator') return userRole === 'operator' || userRole === 'admin'
28+
if (requiredRole === 'developer') return userRole === 'developer' || userRole === 'admin'
29+
return false
30+
}
31+
32+
/**
33+
* Check if a user or preview role has access based on a roles array.
34+
* This is the unified function that handles both normal user access and admin preview mode.
35+
*/
36+
export function hasRoleAccess(
37+
user: UserExt | null | undefined,
38+
roles?: Role[],
39+
previewRole?: Role
40+
): boolean {
41+
// No roles specified = available to everyone
42+
if (!roles || roles.length === 0) return true
43+
44+
// If previewRole is provided, use it (admin preview mode)
45+
// Otherwise, derive role from user
46+
const effectiveRole = previewRole ?? getUserEffectiveRole(user)
47+
if (!effectiveRole) return false
48+
49+
// Check if effective role has any of the required roles
50+
return roles.some((role) => checkRoleMatch(effectiveRole, role))
51+
}
52+
53+
/**
54+
* Check if a preview role has access based on a roles array.
55+
* Used by admins to preview what other roles can see.
56+
* This is a convenience wrapper around hasRoleAccess for preview mode.
57+
*/
58+
export function hasRoleAccessForPreview(
59+
previewRole: Role,
60+
roles?: Role[]
61+
): boolean {
62+
return hasRoleAccess(null, roles, previewRole)
63+
}
64+

frontend/src/routes/(root)/(logged)/tutorials/+page.svelte

Lines changed: 15 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,13 @@
2323
import { RefreshCw, CheckCheck, CheckCircle2, Circle, Shield, Code, UserCog } from 'lucide-svelte'
2424
import { TUTORIALS_CONFIG, type TabId } from '$lib/tutorials/config'
2525
import { userStore } from '$lib/stores'
26-
import type { UserExt } from '$lib/stores'
2726
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
2827
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
28+
import { hasRoleAccess, hasRoleAccessForPreview, type Role } from '$lib/tutorials/roleUtils'
2929
3030
// Role override for admins to preview what other roles see
3131
// Only used when user is admin - defaults to 'admin' (their actual role)
32-
let roleOverride: 'admin' | 'developer' | 'operator' = $state('admin')
33-
34-
/**
35-
* Check if a user has access based on a roles array.
36-
*/
37-
function hasRoleAccess(
38-
user: UserExt | null | undefined,
39-
roles?: ('admin' | 'developer' | 'operator')[]
40-
): boolean {
41-
// No roles specified = available to everyone
42-
if (!roles || roles.length === 0) return true
43-
if (!user) return false
44-
45-
// Check if user has any of the required roles
46-
return roles.some((role) => {
47-
if (role === 'admin') return user.is_admin
48-
if (role === 'operator') return user.operator || user.is_admin
49-
if (role === 'developer') return !user.operator || user.is_admin
50-
return false
51-
})
52-
}
32+
let roleOverride: Role = $state('admin')
5333
5434
// Debug: Log user role for troubleshooting
5535
$effect(() => {
@@ -65,36 +45,25 @@
6545
})
6646
6747
/**
68-
* Check if a preview role has access based on a roles array.
69-
* Used by admins to preview what other roles can see.
48+
* Check if the current user (or preview role) has access to a roles array.
49+
* Handles both normal access and admin preview mode.
7050
*/
71-
function hasRoleAccessForPreview(
72-
previewRole: 'admin' | 'developer' | 'operator',
73-
roles?: ('admin' | 'developer' | 'operator')[]
74-
): boolean {
75-
// No roles specified = available to everyone
76-
if (!roles || roles.length === 0) return true
77-
78-
// Check if preview role has any of the required roles
79-
return roles.some((role) => {
80-
if (role === 'admin') return previewRole === 'admin'
81-
if (role === 'operator') return previewRole === 'operator' || previewRole === 'admin'
82-
if (role === 'developer') return previewRole === 'developer' || previewRole === 'admin'
83-
return false
84-
})
51+
function checkAccess(roles?: Role[]): boolean {
52+
const user = $userStore
53+
// Use preview function if admin has selected a role override
54+
if (user?.is_admin && roleOverride !== 'admin') {
55+
return hasRoleAccessForPreview(roleOverride, roles)
56+
}
57+
return hasRoleAccess(user, roles)
8558
}
8659
8760
// Get active tabs only (filtered by active and roles)
8861
const activeTabs = $derived.by(() => {
89-
const user = $userStore
9062
return Object.entries(TUTORIALS_CONFIG).filter(([, config]) => {
9163
// Filter by active
9264
if (config.active === false) return false
93-
// Filter by roles - use preview function if admin has selected a role override
94-
if (user?.is_admin && roleOverride !== 'admin') {
95-
return hasRoleAccessForPreview(roleOverride, config.roles)
96-
}
97-
return hasRoleAccess(user, config.roles)
65+
// Filter by roles
66+
return checkAccess(config.roles)
9867
}) as [TabId, typeof TUTORIALS_CONFIG[TabId]][]
9968
})
10069
@@ -119,11 +88,7 @@
11988
const visibleTutorials = $derived(
12089
currentTabConfig.tutorials.filter((tutorial) => {
12190
if (tutorial.active === false) return false
122-
// Use preview function if admin has selected a role override
123-
if ($userStore?.is_admin && roleOverride !== 'admin') {
124-
return hasRoleAccessForPreview(roleOverride, tutorial.roles)
125-
}
126-
return hasRoleAccess($userStore, tutorial.roles)
91+
return checkAccess(tutorial.roles)
12792
})
12893
)
12994
@@ -238,18 +203,12 @@
238203
// Calculate progress for each tab
239204
function getTabProgress(tabId: TabId) {
240205
const tabConfig = TUTORIALS_CONFIG[tabId]
241-
const user = $userStore
242206
243207
// Get all tutorial indexes for this tab (filtered by role)
244208
const indexes: number[] = []
245209
for (const tutorial of tabConfig.tutorials) {
246210
if (tutorial.active === false || tutorial.index === undefined) continue
247-
// Use preview function if admin has selected a role override
248-
if (user?.is_admin && roleOverride !== 'admin') {
249-
if (!hasRoleAccessForPreview(roleOverride, tutorial.roles)) continue
250-
} else {
251-
if (!hasRoleAccess(user, tutorial.roles)) continue
252-
}
211+
if (!checkAccess(tutorial.roles)) continue
253212
indexes.push(tutorial.index)
254213
}
255214

0 commit comments

Comments
 (0)