-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor: Update UserRole enum to ProjectRole #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughReplaced UserRole with Role from the generated Prisma client across actions, libs, and UI. Updated permission checks and requiredRoles usage. Added concrete inviteMember logic to verify users and create project memberships. Adjusted membership semantics from MEMBER→USER. Updated UI components to use Role values and defaults. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant D as InviteMemberDialog (UI)
participant A as inviteMember (action)
participant P as protectedAction
participant R as Permissions
participant DB as Prisma Client
U->>D: Submit invite (email, role=Role.USER)
D->>A: invoke inviteMember(input)
A->>P: protectedAction({ projectId, requiredRoles:[Role.ADMIN] })
P->>R: hasProjectPermission(user.role, [Role.ADMIN])
R-->>P: boolean (allow/deny)
alt authorized
A->>DB: findUserByEmail(email)
alt user found
A->>DB: create projectMember(projectId, userId, role)
DB-->>A: projectMember
A-->>D: success
else user not found
A-->>D: error (user not found)
end
else unauthorized
P-->>D: error (forbidden)
end
sequenceDiagram
autonumber
participant C as getProjectMembers (action)
participant P as protectedAction
participant R as Permissions
participant DB as Prisma Client
C->>P: protectedAction({ requiredRoles:[Role.USER, Role.ADMIN] })
P->>R: hasProjectPermission(user.role, [USER, ADMIN])
R-->>P: allow/deny
alt allowed
C->>DB: query members by projectId
DB-->>C: members list
C-->>Caller: members list
else denied
P-->>Caller: error (forbidden)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/lib/action.ts (1)
67-98: Authorization bypass: requiredRoles read from parsedInput instead of metadata.All protected actions pass requiredRoles via the 2nd argument, but middleware checks parsedInput, so it skips auth and allows every call. Read from metadata (and derive projectId from metadata or input).
Apply this diff:
-export const protectedAction = authActionClient.use(async ({ next, ctx, parsedInput }) => { - const { projectId, requiredRoles } = parsedInput as { projectId: string; requiredRoles: Role[] }; +export const protectedAction = authActionClient.use(async ({ next, ctx, parsedInput, metadata }) => { + // Metadata supplied by the action definition (2nd arg) + const meta = metadata as + | { requiredRoles?: Role[]; projectId?: string | ((input: unknown) => string) } + | undefined; + const requiredRoles = meta?.requiredRoles; + const projectIdFromMeta = meta?.projectId; + const projectId = + typeof projectIdFromMeta === 'function' + ? projectIdFromMeta(parsedInput) + : projectIdFromMeta ?? (parsedInput as any)?.projectId; - if (!projectId || !requiredRoles) { + if (!projectId || !requiredRoles?.length) { // If projectId or requiredRoles are not provided, it's not a project-protected action return next({ ctx: { ...ctx, } }); } const projectMember = await prisma.projectMember.findUnique({ where: { projectId_userId: { projectId, userId: ctx.user.id, }, }, }); if (!projectMember || !hasProjectPermission(projectMember.role, requiredRoles)) { throw new Error('Not authorized to perform this action on this project.'); } return next({ ctx: { ...ctx, projectMember, } }); });Optionally, widen metadata validation to include requiredRoles:
export function defineMetadataSchema() { - return zod.object({ - actionName: zod.string() - }); + return zod.object({ + actionName: zod.string().optional(), + requiredRoles: zod.array(zod.nativeEnum(Role)).optional(), + // projectId may be a function; skip validation here + }); }src/components/models/projects/InviteMemberDialog.tsx (1)
20-27: Fix form typing and schema mismatch (email missing in schema).
- useForm uses a value as a type. Use a proper inferred type.
- The action schema currently lacks email; resolver will reject/mismatch. Align with schema after adding email.
Apply this diff here after exporting a type from actions:
-import { inviteMember, inviteMemberSchema } from "@/actions/collaboration"; +import { inviteMember, inviteMemberSchema } from "@/actions/collaboration"; +import type { InviteMemberInput } from "@/actions/collaboration"; - const form = useForm<inviteMemberSchema>({ + const form = useForm<InviteMemberInput>({ resolver: zodResolver(inviteMemberSchema), defaultValues: { projectId, email: "", role: Role.USER, }, });Follow-up diff in actions to export the type is included in that file’s comment.
🧹 Nitpick comments (3)
src/actions/collaboration.ts (3)
36-47: Rename unused context param to satisfy linter.
userisn’t used. Rename to_user.-export const removeMember = protectedAction(removeMemberSchema, { projectId: (input) => input.projectId, requiredRoles: [Role.ADMIN] }, async ({ projectId, memberId }, { user }) => { +export const removeMember = protectedAction( + removeMemberSchema, + { projectId: (input) => input.projectId, requiredRoles: [Role.ADMIN] }, + async ({ projectId, memberId }, { user: _user }) => {
55-69: Rename unused context param to satisfy linter.-export const updateMemberRole = protectedAction(updateMemberRoleSchema, { projectId: (input) => input.projectId, requiredRoles: [Role.ADMIN] }, async ({ projectId, memberId, role }, { user }) => { +export const updateMemberRole = protectedAction( + updateMemberRoleSchema, + { projectId: (input) => input.projectId, requiredRoles: [Role.ADMIN] }, + async ({ projectId, memberId, role }, { user: _user }) => {
75-84: Broadened read access to USER and ADMIN—LGTM. Also fix unused arg.-export const getProjectMembers = protectedAction(getProjectMembersSchema, { projectId: (input) => input.projectId, requiredRoles: [Role.USER, Role.ADMIN] }, async ({ projectId }, { user }) => { +export const getProjectMembers = protectedAction( + getProjectMembersSchema, + { projectId: (input) => input.projectId, requiredRoles: [Role.USER, Role.ADMIN] }, + async ({ projectId }, { user: _user }) => {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
src/actions/collaboration.ts(4 hunks)src/components/models/projects/InviteMemberDialog.tsx(3 hunks)src/components/models/projects/ProjectMembersTable.tsx(4 hunks)src/lib/action.ts(2 hunks)src/lib/permissions.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/actions/collaboration.ts (1)
src/lib/action.ts (1)
protectedAction(67-98)
🪛 GitHub Check: lint-and-build
src/actions/collaboration.ts
[warning] 11-11:
'user' is defined but never used. Allowed unused args must match /^_/u
[warning] 36-36:
'user' is defined but never used. Allowed unused args must match /^_/u
[warning] 55-55:
'user' is defined but never used. Allowed unused args must match /^_/u
[warning] 75-75:
'user' is defined but never used. Allowed unused args must match /^_/u
🔇 Additional comments (6)
src/lib/action.ts (1)
6-6: Import swap to Role looks fine.Enum import aligns with the rest of the PR.
src/components/models/projects/InviteMemberDialog.tsx (2)
4-4: Import aligned with new enum.Switch to Role is consistent.
25-26: Default and options updated to Role—LGTM.Role.USER default and options mapped from Role satisfy the new enum.
Also applies to: 54-55
src/components/models/projects/ProjectMembersTable.tsx (1)
3-3: Enum migration to Role is consistent and correct.
- Types and calls updated to Role.
- Passing member.user.id matches server expecting userId in unique key.
Also applies to: 18-19, 50-58, 86-91
src/lib/permissions.ts (1)
1-13: Permissions updated to Role with correct semantics.
- hasProjectPermission via includes is fine.
- isProjectMember allowing USER or ADMIN matches new roles.
src/actions/collaboration.ts (1)
49-56: Schema change to Role is correct. Also fix unused arg in handler below.Ensure updateMemberRole callers pass valid Role values (UI now does).
| import { Role } from "@/generated/prisma/client"; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Verify migration completeness (UserRole → Role/ProjectRole) and build health.
Run a quick repo scan for any leftover UserRole/ProjectRole references and imports from @prisma/client, and spot type issues like useForm generic misuse.
Also applies to: 11-12, 36-37, 55-56, 75-76
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) Check for leftover UserRole/ProjectRole usages"
rg -nP -C2 '\b(UserRole|ProjectRole)\b'
echo
echo "2) Check for Role imports from @prisma/client (should be from generated path now)"
rg -nP -C0 'import\s*{[^}]*\b(Role|UserRole|ProjectRole)\b[^}]*}\s*from\s*[\"\']@prisma/client[\"\']'
echo
echo "3) Check for useForm generic mis-uses referencing schema values"
rg -nP -C3 'useForm<\s*inviteMemberSchema\s*>' --type=ts --type=tsx
echo
echo "4) Spot actions that destructure unused { user }"
rg -nP -C2 'async\s*\([^)]*\{\s*user\s*\}\s*\)' src/actions
echo
echo "5) Sanity-check that all action calls pass requiredRoles in options"
rg -nP -n 'protectedAction\([^,]+,\s*\{[^}]*requiredRoles' src/actions
echo
echo "6) Grep for any remaining z.nativeEnum\(UserRole\)"
rg -nP -C2 'z\.nativeEnum\(\s*UserRole\s*\)'Length of output: 49
🏁 Script executed:
#!/bin/bash
echo "1) Check for leftover UserRole/ProjectRole usages"
rg -nP -C2 '\b(UserRole|ProjectRole)\b' || true
echo
echo "2) Check for Role imports from @prisma/client (should be from generated path now)"
rg -nP -C0 "import\s*{[^}]*\b(Role|UserRole|ProjectRole)\b[^}]*}\s*from\s*['\"]@prisma/client['\"]" || true
echo
echo "3) Check for useForm generic mis-uses referencing schema values"
rg -nP -C3 'useForm<\s*inviteMemberSchema\s*>' --type=ts --type=tsx || true
echo
echo "4) Spot actions that destructure unused { user }"
rg -nP -C2 'async\s*\([^)]*\{\s*user\s*\}\s*\)' src/actions || true
echo
echo "5) Sanity-check that all action calls pass requiredRoles in options"
rg -nP -n 'protectedAction\([^,]+,\s*\{[^}]*requiredRoles' src/actions || true
echo
echo "6) Grep for any remaining z.nativeEnum\(UserRole\)"
rg -nP -C2 'z\.nativeEnum\(\s*UserRole\s*\)' || trueLength of output: 3035
🏁 Script executed:
#!/bin/bash
echo "Re-check for useForm generic mis-uses referencing schema values without --type flags"
rg -nP 'useForm<\s*inviteMemberSchema\s*>' -g '*.ts' -g '*.tsx' || trueLength of output: 186
Migration complete; finalize cleanup
- No leftover
UserRole/ProjectRoleimports or references; allprotectedActioncalls includerequiredRoles. - Drop unused
{ user }destructuring insrc/actions/collaboration.ts(lines 11–12, 36–37, 55–56, 75–76) or utilizeuser. - In
src/components/models/projects/InviteMemberDialog.tsx:20, replaceuseForm<inviteMemberSchema>withuseForm<z.infer<typeof inviteMemberSchema>>().
🤖 Prompt for AI Agents
In src/actions/collaboration.ts around lines 11–12, 36–37, 55–56 and 75–76 there
are unused destructured { user } variables; remove the unused { user } entries
from those function parameter lists (or, if intended, use the user variable in
the function body). Ensure each protectedAction call keeps requiredRoles intact
and run TypeScript compile to confirm no unused-variable errors remain.
| export const inviteMemberSchema = z.object({ | ||
| projectId: z.string(), | ||
| email: z.string().email(), | ||
| role: z.nativeEnum(UserRole), | ||
| role: z.nativeEnum(Role), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add email to schema and export inferred input type.
Handler and UI require email, but schema omits it, causing type/validation errors.
Apply this diff:
export const inviteMemberSchema = z.object({
projectId: z.string(),
+ email: z.string().email(),
role: z.nativeEnum(Role),
});
-export const inviteMember = protectedAction(inviteMemberSchema, { projectId: (input) => input.projectId, requiredRoles: [Role.ADMIN] }, async ({ projectId, email, role }, { user }) => {
+export type InviteMemberInput = z.infer<typeof inviteMemberSchema>;
+
+export const inviteMember = protectedAction(
+ inviteMemberSchema,
+ { projectId: (input) => input.projectId, requiredRoles: [Role.ADMIN] },
+ async ({ projectId, email, role }, { user: _user }) => {
// TODO: Implement actual invitation logic (e.g., send email, create invitation token)
// For now, directly add the user if they exist
const existingUser = await prisma.user.findUnique({ where: { email } });
if (!existingUser) {
throw new Error("User not found");
}
- await prisma.projectMember.create({
+ // Consider handling duplicates gracefully; upsert can avoid unique constraint errors
+ await prisma.projectMember.create({
data: {
projectId,
userId: existingUser.id,
role,
},
});
return { success: true, message: `User ${email} invited as ${role}` };
-});
+});Also applies to: 11-29
🤖 Prompt for AI Agents
In src/actions/collaboration.ts around lines 6-9 (and also affecting lines
11-29), the inviteMemberSchema is missing the required email field and the
inferred input type is not exported; update the schema to include email:
z.string().email() (or z.string().email().nonempty() per project conventions),
export the schema if not already exported, and add an exported type alias like
export type InviteMemberInput = z.infer<typeof inviteMemberSchema> so handlers
and UI can import the validated/typed input.
The
UserRoleenum has been renamed toProjectRolein@prisma/client. This change updates all references to the oldUserRoleenum withinsrc/actions/collaboration.tsto use the newProjectRoleidentifier.Specifically, this includes:
z.nativeEnum(UserRole)withz.nativeEnum(ProjectRole).requiredRolesarrays and any inline role variables.Review Notes:
Please verify that all instances of
UserRolehave been correctly updated toProjectRoleand that the application builds without TypeScript errors.Summary by CodeRabbit