@@ -6,6 +6,7 @@ import { headers } from 'next/headers';
66import { redirect } from 'next/navigation' ;
77import {
88 type UserPermissions ,
9+ canAccessAuditorView ,
910 canAccessRoute ,
1011 getDefaultRoute ,
1112 mergePermissions ,
@@ -92,3 +93,82 @@ export async function requireRoutePermission(
9293 redirect ( defaultRoute ?? '/no-access' ) ;
9394 }
9495}
96+
97+ /**
98+ * CS-189: Resolve only the permissions granted by the user's CUSTOM org
99+ * roles (i.e. not from built-in roles). Needed for the Auditor View
100+ * visibility rule, which wants to know whether a custom role explicitly
101+ * grants `audit:read` — owner/admin's implicit all-permissions don't count.
102+ */
103+ export async function resolveCustomRolePermissions (
104+ roleString : string | null | undefined ,
105+ orgId : string ,
106+ ) : Promise < UserPermissions > {
107+ const { customRoleNames } = resolveBuiltInPermissions ( roleString ) ;
108+ const result : UserPermissions = { } ;
109+ if ( customRoleNames . length === 0 ) return result ;
110+
111+ const customRoles = await db . organizationRole . findMany ( {
112+ where : { organizationId : orgId , name : { in : customRoleNames } } ,
113+ select : { permissions : true } ,
114+ } ) ;
115+
116+ for ( const role of customRoles ) {
117+ if ( ! role . permissions ) continue ;
118+ const parsed =
119+ typeof role . permissions === 'string'
120+ ? JSON . parse ( role . permissions )
121+ : role . permissions ;
122+ if ( parsed && typeof parsed === 'object' ) {
123+ mergePermissions ( result , parsed as Record < string , string [ ] > ) ;
124+ }
125+ }
126+ return result ;
127+ }
128+
129+ /**
130+ * Server-side Auditor View access check. Mirrors the client-side
131+ * `canAccessAuditorView` but pulls the custom-role permissions from the
132+ * DB for the current user. Returns null if the user isn't in the org.
133+ */
134+ export async function resolveAuditorViewAccess (
135+ orgId : string ,
136+ ) : Promise < { canAccess : boolean ; roleString : string | null } | null > {
137+ const session = await auth . api . getSession ( {
138+ headers : await headers ( ) ,
139+ } ) ;
140+ if ( ! session ?. user ?. id ) return null ;
141+
142+ const member = await db . member . findFirst ( {
143+ where : {
144+ userId : session . user . id ,
145+ organizationId : orgId ,
146+ deactivated : false ,
147+ } ,
148+ select : { role : true } ,
149+ } ) ;
150+ if ( ! member ) return null ;
151+
152+ const customPerms = await resolveCustomRolePermissions ( member . role , orgId ) ;
153+ return {
154+ canAccess : canAccessAuditorView ( member . role , customPerms ) ,
155+ roleString : member . role ,
156+ } ;
157+ }
158+
159+ /**
160+ * Route guard for the Auditor View page. Replaces `requireRoutePermission(
161+ * 'auditor', orgId)` — the plain permission check let owner/admin through
162+ * via their implicit `audit:read`. This helper enforces the stricter
163+ * "built-in auditor OR custom role with audit:read" rule.
164+ */
165+ export async function requireAuditorViewAccess ( orgId : string ) : Promise < void > {
166+ const result = await resolveAuditorViewAccess ( orgId ) ;
167+ if ( result ?. canAccess ) return ;
168+
169+ const permissions = await resolveCurrentUserPermissions ( orgId ) ;
170+ const defaultRoute = permissions
171+ ? getDefaultRoute ( permissions , orgId )
172+ : null ;
173+ redirect ( defaultRoute ?? '/no-access' ) ;
174+ }
0 commit comments