Skip to content

AUTH-9 - Implemented UserProject server actions - #5

Open
leec18 wants to merge 2 commits into
mainfrom
auth-9
Open

AUTH-9 - Implemented UserProject server actions#5
leec18 wants to merge 2 commits into
mainfrom
auth-9

Conversation

@leec18

@leec18 leec18 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

Created lib/userProject.ts for managing the UserProject join table which contains:

  • createUserProject: prevents duplicate assignments by checking for an existing record before inserting
  • getUserProject
  • getUserProjects: optional filtering by userId or projectId
  • deleteUserProject

Comment thread src/app/login/EmailForm.tsx Outdated
}

export function EmailForm({ onNext }: Props) {
const [email, setEmail] = useState("");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets install react hook form and zod (or other packages if you have better ones) for this to avoid having useStates cause that gets out of hand very quickly

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can integrate with shadcn here too. They seem to support both react hook form and tanstack form so take your pick! https://ui.shadcn.com/docs/forms

Comment thread src/app/login/EmailForm.tsx Outdated
</button>
</form>
);
} No newline at end of file

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new line end of file please

Comment thread src/app/login/EmailForm.tsx Outdated
onChange={(e) => setEmail(e.target.value)}
required
/>
<button

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets install the shadcn button from here

Comment thread src/app/globals.css Outdated
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you set up a tailwind config and colors file? Guide: https://tailwindcss.com/docs/installation/framework-guides/nextjs

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of our colors for the application should be defined in this file (e.g. primary, secondary, accent, etc) and then referenced by those names throughout all files

Comment thread src/app/login/page.tsx Outdated
</div>
</main>
);
} No newline at end of file

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add newlines at the ends of all files

Comment thread src/lib/userProject.ts Outdated

async function createUserProject(userId: string, projectId: string) {
const existing = await prisma.userProject.findUnique({
where: { userId_projectId: { userId, projectId } }

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets move this inline

Comment thread src/lib/userProject.ts
}

async function getUserProject(userId: string, projectId: string) {
return await prisma.userProject.findUnique({

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also move inline, anything this short can be inline

Comment thread src/lib/userProject.ts Outdated
});
}

async function getUserProjects(filters?: { userId?: string, projectId?: string }) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for simplicity, lets destructure the args in the function definition so go { userId?, projectId? } : { userId: ..., projectId: string } or however it goes

Comment thread src/lib/userProject.ts Outdated
}

async function deleteUserProject(userId: string, projectId: string) {
return await prisma.userProject.delete({

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have soft deletes for this or not on this one?

Comment thread src/lib/userProject.ts Outdated
});
}

async function deleteUserProject(userId: string, projectId: string) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be better off being named removeUserFromProject

@pataniaeli pataniaeli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review written by a Claude agent.

Quick note before the rest: none of the four functions in this file are exported yet, so as merged today this genuinely is inert — nothing can call it. That's consistent with a "land the data layer now, wire up an authenticated action layer in a follow-up" plan, which seems like a reasonable way to sequence this. The comments below are mostly about what needs to be true by the time export gets added (which the PR title suggests is coming soon), plus one thing that blocks the build regardless.

Must-fix regardless of sequencing:

  • This doesn't currently build — tsc fails with three TS2353 errors from deletedAt being nested inside the compound-key where clause (see inline comment on createUserProject; same pattern repeats in getUserProject and removeUserFromProject). Worth fixing before merge independent of anything else here.
  • The upsert in createUserProject silently un-revokes a soft-deleted membership with no audit trail (see inline comment).
  • No input validation — zod is already a dependency elsewhere in the repo.
  • prettier --check currently fails on this file (brace spacing, trailing whitespace) — this repo's CLAUDE.md treats format:check as a merge gate.

Pending authz ticket / for whenever export gets added (see note above):

  • createUserProject/removeUserFromProject will need a caller-identity + authority check the moment they're exported, since this table is the authorization-scoping mechanism for the whole multi-tenant system (see inline comment).
  • getUserProjects({}) currently has no scoping and would return every membership across every project once reachable.

Worth its own ticket, not introduced by this PR: same RLS gap flagged on the other open PRs — UserProject sits in the same unprotected schema, which matters even more here since this table is the actual access-control list.

(Edit: corrected the inline comment anchors below that were off in my first pass — same content, right lines now.)

Comment thread src/lib/userProject.ts
import { prisma } from "@/lib/prisma";

async function createUserProject(userId: string, projectId: string) {
const existing = await prisma.userProject.findUnique({ where: { userId_projectId: { userId, projectId, deletedAt: null } }});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deletedAt: null is nested inside the compound-key object here, but Prisma's generated UserProjectUserIdProjectIdCompoundUniqueInput type only has userId/projectId — running tsc --noEmit against this branch fails here (and at the matching spots in getUserProject/removeUserFromProject) with TS2353: Object literal may only specify known properties. This one's independent of anything else in the review — npm run build won't pass as-is. The fix is to move deletedAt: null up a level, alongside the compound key (where: { userId_projectId: { userId, projectId }, deletedAt: null }), not to just delete it — deleting it would make a soft-deleted membership permanently block re-adding that user (see the upsert note below).

Comment thread src/lib/userProject.ts
if (existing) {
throw new Error("User project already exists");
}
return await prisma.userProject.upsert({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the type error above is fixed, this upsert will silently resurrect a previously soft-deleted membership (update: { deletedAt: null }) with no change to createdAt and no audit trail that access was ever removed and re-granted. Worth setting a fresh timestamp (or a dedicated restoredAt field) on the update branch so a re-grant is visible later, independent of whatever authorization ends up wrapping this function.

Comment thread src/lib/userProject.ts
"use server";
import { prisma } from "@/lib/prisma";

async function createUserProject(userId: string, projectId: string) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed none of the four functions in this file are actually exported yet, so as merged this is dead code and not reachable — which reads like this might genuinely be the "data layer first, action layer later" pattern. If that's the plan, just want to flag it explicitly: whenever export gets added here (which the PR title implies is coming), it'll need a caller-identity + authority check attached at the same time, since UserProject is the authorization-scoping table for the whole system — an unauthenticated grant/revoke here is more consequential than on the other open PRs. Also worth scoping getUserProjects (line 22) to the caller once it's exposed — an empty {} filter currently returns every membership across all projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants