Implement CASL Authorization management with recruitment-session-specific roles - #47
Implement CASL Authorization management with recruitment-session-specific roles#47cristiansap wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces recruitment-session-scoped authorization using CASL, so access is determined by the user’s role within a specific recruitment session (via a new join table), and it renames the “Users” domain to “Members” across the dashboard and authorization subjects.
Changes:
- Added
users_to_recruiting_sessionsand role resolution (findUserRoleForSession) to compute permissions per recruitment session. - Replaced simple role checks with CASL abilities for page access, sidebar link visibility, and multiple server actions.
- Renamed “Users” → “Members” in routing/UI and improved the recruitment session switcher redirect behavior.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/middleware.ts | Redirect unauthenticated users to /signin for / and /dashboard/*. |
| src/lib/services/recruitmentSessions.ts | Adds session-scoped role lookup (findUserRoleForSession). |
| src/lib/server/authTypes.ts | Renames User role to Member and comments out God. |
| src/lib/helpers/pageAuthorization.ts | Introduces requirePageAccess() guard based on CASL ability. |
| src/lib/auth.ts | Updates Better Auth React client base path to /api/auth. |
| src/lib/actions/users.ts | Protects member-related action with session-scoped ability check. |
| src/lib/actions/recruitmentSessions.ts | Adds ability check before creating recruitment sessions. |
| src/lib/actions/interviews.ts | Adds session-scoped ability check via applicant’s session. |
| src/lib/actions/availability.ts | Adds session-scoped ability check for availability submission. |
| src/lib/actions/applicants.ts | Adds session-scoped ability checks for applicant stage transitions. |
| src/lib/abilities/user.ts | Defines CASL actions/subjects and role-based permissions. |
| src/lib/abilities/server.ts | Adds server helpers to build abilities from session-scoped role. |
| src/lib/abilities/index.ts | Simplifies exports after removing old A<> helper type. |
| src/db/types.ts | Adds types for usersToRecruitingSessions. |
| src/db/seed.ts | Extends seed script with session-role assignment flags and logic. |
| src/db/schema.ts | Adds users_to_recruiting_sessions table and relations. |
| src/components/ability/AbilityContext.tsx | Types AbilityContext as AppAbility. |
| src/app/page.tsx | Redirects to /dashboard if authenticated, otherwise /signin. |
| src/app/dashboard/layout.tsx | Simplifies layout props typing and keeps auth gate. |
| src/app/dashboard/[rid]/users/page.tsx | Removes old /users page under session dashboard. |
| src/app/dashboard/[rid]/Sidebar.tsx | Updates link gating to CASL Can with subjects; adds /members link. |
| src/app/dashboard/[rid]/Sidebar.data.tsx | Refactors sidebar links to map to CASL subjects. |
| src/app/dashboard/[rid]/RecruitmentSwitcher.tsx | Switches sessions by routing to /dashboard/${rid} overview. |
| src/app/dashboard/[rid]/page.tsx | Renders role-specific overview component per session role. |
| src/app/dashboard/[rid]/members/page.tsx | Adds members page guarded via requirePageAccess. |
| src/app/dashboard/[rid]/members/MembersTable.tsx | Updates table to work with members page and action signature (passes rid). |
| src/app/dashboard/[rid]/MemberOverview.tsx | Adds member-specific overview placeholder component. |
| src/app/dashboard/[rid]/GuestOverview.tsx | Adds guest-specific overview placeholder component. |
| src/app/dashboard/[rid]/ClerkOverview.tsx | Adds clerk-specific overview placeholder component. |
| src/app/dashboard/[rid]/AdminOverview.tsx | Adds admin-specific overview placeholder component. |
| src/app/dashboard/[rid]/candidates/page.tsx | Guards candidates list page with requirePageAccess. |
| src/app/dashboard/[rid]/candidates/[id]/page.tsx | Guards candidate details page with requirePageAccess. |
| src/app/dashboard/[rid]/availability/page.tsx | Guards availability overview page with requirePageAccess. |
| src/app/dashboard/[rid]/me/availability/page.tsx | Copy update (English text) on availability page. |
| src/app/dashboard/[rid]/layout.tsx | Resolves session role for the AbilityProvider and sidebar display. |
| docs/README.md | Updates formatting and documents new seed flags for session roles. |
Comments suppressed due to low confidence (2)
src/db/seed.ts:123
- When running the seed script only to assign roles (i.e.
shouldSeedDatais false), it still mutates existing recruitment sessions by clampingend_datetostart_date + 7 days. This is a surprising side effect for a role-assignment run. Consider guarding the whole "update sessions" and "generate timeslots" sections behindshouldSeedDataas well.
// Update recruitment sessions to ensure start_date and end_date are at most 1 week apart
const recruitingSessions = await db.select().from(schema.recruitingSession);
for (const session of recruitingSessions) {
const startDate = new Date(session.start_date);
const maxEndDate = new Date(startDate);
maxEndDate.setDate(maxEndDate.getDate() + 7);
const currentEndDate = new Date(session.end_date);
// If end_date is more than 1 week after start_date, update it
if (currentEndDate > maxEndDate) {
await db
.update(schema.recruitingSession)
.set({ end_date: maxEndDate })
.where(eq(schema.recruitingSession.id, session.id));
session.end_date = maxEndDate;
}
}
src/app/dashboard/[rid]/members/MembersTable.tsx:81
w-50is not a default Tailwind width utility (defaults includew-48,w-52, etc.). If the project isn't extending the spacing scale, this class will be ignored and the column width won't apply. Use a valid utility (e.g.w-52) or an arbitrary value (w-[50px]/w-[12.5rem]) instead.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pasc4le
left a comment
There was a problem hiding this comment.
Good PR! Thank you. Sorry for submitting so many comments, it's just a big PR. Pretty good implementation though, I love the new approach that you've taken, much cleaner that what I did before.
| export default async function AvailabilityOverviewPage({ | ||
| params, | ||
| }: PageProps<'/dashboard/[rid]/availability'>) { | ||
| const session = await auth.api.getSession({ |
There was a problem hiding this comment.
Maybe it would be better to move the page checks to the middleware? Or dashboard layout?
There was a problem hiding this comment.
I'd not move requirePageAccess to the middleware or layout because it queries the DB with both the userId and the recruitingSessionId (rid) to get the role for that specific session (and it's easy to get the rid from this page). So I'd leave it there.
There was a problem hiding this comment.
The recruitmentSessionId is also retrievable from the dashboard/[rid]/layout.tsx.
There was a problem hiding this comment.
True, but requirePageAccess also takes a page-specific subject (e.g. 'AvailabilityOverviewPage') as argument. However, the layout doesn't know which page it's currently rendering, so it can't know which subject to pass to requirePageAccess. Moving it there would mean either calling it for every possible subject (which makes no sense) or refactoring the permission granularity entirely.
| }) | ||
| ); | ||
|
|
||
| export const usersToRecruitingSessions = pgTable( |
There was a problem hiding this comment.
We should also remove the old columns from the 'user' table in auth. It is configured by better-auth at lib/server/auth.ts, if you want I can take this one later on as a fix.
4e48cbf to
18a884b
Compare
This PR introduces recruitment-session-specific roles by adding the required database model, resolving the current user's role per recruitment session, and replacing simple role checks with CASL-based authorization across pages, sidebar links, server actions, and API routes.
Moreover, it refactors the "Users" domain model to "Members" for semantic clarity, and improves user experience when switching recruitment sessions.
Here is the list of implemented changes:
Authorization & Security
abilityForUserInSession()server helper to resolve CASL abilities based on the session role.requirePageAccess()guard on all protected dashboard pages (Members, Candidates, Availability).HTTP 403 (Forbidden);404 (Not Found)behavior for non-existent pages is preserved.Role & Naming Refactor
User→Memberacross types, services, database, and seed logic.Godrole from code logic (kept commented in enum for future use).Routes & UI
/users→/membersto align with the new Member role naming.GuestOverview,MemberOverview,ClerkOverview,AdminOverview) so the dashboard root renders different content based on the session role./membersroute.Database & Infrastructure
users_to_recruiting_sessionstable to store recruitment-session-scoped roles per user.--session-role,--session-id) to assign test roles.