-
Notifications
You must be signed in to change notification settings - Fork 1
Fix/tutor profile registering #39
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
base: main
Are you sure you want to change the base?
Changes from all commits
478ec0e
0d5b6b4
a266f44
13cd5a7
c27d9ee
5ed80b5
0fc2990
b575bf8
1daa171
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "TutorProfile" ADD COLUMN "preferredSessionTypes" TEXT[]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import Link from "next/link"; | |
| import { usePathname } from "next/navigation"; | ||
| import { Home, Activity, BookOpen, MessageSquare, Phone, Settings } from "lucide-react"; | ||
| import { useState } from "react"; | ||
| import { api } from "@/trpc/react"; | ||
|
|
||
| type Item = { | ||
| href: string; | ||
|
|
@@ -22,9 +23,17 @@ const items: Item[] = [ | |
| export function Sidebar() { | ||
| const pathname = usePathname(); | ||
| const [open, setOpen] = useState(false); | ||
| const { data: role } = api.signal.getViewerRole.useQuery(); | ||
| const isTutor = role === "TUTOR"; | ||
|
Comment on lines
+26
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle loading state and consider extracting the role query logic. The role query pattern is duplicated in both Solution 1: Add loading state handling - const { data: role } = api.signal.getViewerRole.useQuery();
- const isTutor = role === "TUTOR";
+ const { data: role, isLoading } = api.signal.getViewerRole.useQuery();
+ const isTutor = role === "TUTOR";Then render a loading indicator in the badge area while Solution 2 (recommended): Extract to a custom hook to reduce duplication Create import { api } from "@/trpc/react";
export function useViewerRole() {
const { data: role, isLoading, error } = api.signal.getViewerRole.useQuery();
const isTutor = role === "TUTOR";
return { role, isTutor, isLoading, error };
}Then use it in both components: - const { data: role } = api.signal.getViewerRole.useQuery();
- const isTutor = role === "TUTOR";
+ const { isTutor, isLoading } = useViewerRole();🤖 Prompt for AI Agents |
||
|
|
||
| const Nav = ( | ||
| <nav className="rounded-2xl bg-white/90 p-3 shadow-sm backdrop-blur"> | ||
| <div className="px-3 py-4 text-lg font-semibold text-neutral-900">TutorLink</div> | ||
| <div className="px-3 py-4"> | ||
| <div className="text-lg font-semibold text-neutral-900">TutorLink</div> | ||
| <div className="mt-2 inline-block px-2.5 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-900"> | ||
| {isTutor ? "🎓 Tutor" : "👤 Student"} | ||
| </div> | ||
| </div> | ||
| <ul className="space-y-1"> | ||
| {items.map((i) => { | ||
| const active = pathname === i.href; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ import { useEffect } from "react"; | |
| import { useRouter } from "next/navigation"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { useOnboardingStore } from "@/stores/onboarding"; | ||
| import { api } from "@/trpc/react"; | ||
| import { toast } from "sonner"; | ||
|
|
||
| export default function TutorSummary() { | ||
| const role = useOnboardingStore((s) => s.role); | ||
|
|
@@ -13,13 +15,35 @@ export default function TutorSummary() { | |
| const yearsOfExperience = useOnboardingStore((s) => s.yearsOfExperience); | ||
| const teachingStyle = Array.from(useOnboardingStore((s) => s.teachingStyle)); | ||
| const preferredSessionTypes = Array.from(useOnboardingStore((s) => s.preferredSessionTypes)); | ||
| const reset = useOnboardingStore((s) => s.reset); | ||
| const router = useRouter(); | ||
|
|
||
| const { mutate: createProfile, isPending } = api.tutor.createProfile.useMutation({ | ||
| onSuccess: () => { | ||
| toast.success("Profile created successfully!"); | ||
| reset(); | ||
| router.push("/dashboard"); | ||
| }, | ||
| onError: (error) => { | ||
| toast.error(error.message); | ||
| }, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| if (!hydrated) return; | ||
| if (role !== "tutor") router.replace("/onboarding"); | ||
| }, [hydrated, role, router]); | ||
|
|
||
| const handleFinish = () => { | ||
| createProfile({ | ||
| subjectInterests, | ||
| teachingLevels, | ||
| yearsOfExperience: yearsOfExperience ?? 0, | ||
| teachingStyle, | ||
| preferredSessionTypes, | ||
| }); | ||
| }; | ||
|
Comment on lines
+37
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify validation for required profile fields. The function correctly passes onboarding data to the mutation. However, there's no client-side validation before submission. For instance, a tutor could submit with empty Confirm that server-side validation handles these cases appropriately. If so, consider adding client-side validation to improve UX by catching issues before the network request. Run this script to check the server-side validation logic: 🏁 Script executed: #!/bin/bash
# Description: Locate and examine the tutor.createProfile mutation handler to verify validation
# Find the createProfile mutation implementation
ast-grep --pattern 'createProfile: $_()'
# Also search for the procedure/mutation definition
rg -A 20 'createProfile.*=.*' --type tsLength of output: 3160 I need to locate the server-side procedure implementation. Let me search for the tRPC router that defines the Let me search for the tutor router file in the codebase: Now let me search the codebase for the tutor router file: 🏁 Script executed: #!/bin/bash
# Search for tutor router file and createProfile procedure
# Look for files containing tutor router
fd -e ts -e tsx | grep -E '(router|tutor)' | head -20
# Search for createProfile mutation definition
rg -A 30 'createProfile.*mutation\|createProfile.*:.*procedure' --type tsLength of output: 741 Perfect! Found the tutor router. Let me examine it to find the createProfile procedure and its validation: 🏁 Script executed: #!/bin/bash
# Examine the tutor router file for createProfile procedure
cat src/server/api/routers/tutor.tsLength of output: 6297 Fix data loss and strengthen validation in createProfile procedure. Server-side validation exists but has two critical issues:
🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <div className="min-h-screen w-full bg-linear-to-b from-white via-blue-50 to-[#43A8FF]"> | ||
| <div className="mx-auto max-w-7xl px-4 pt-4 sm:px-6 lg:px-8"> | ||
|
|
@@ -58,8 +82,13 @@ export default function TutorSummary() { | |
| <Button asChild variant="brandOutline" className="rounded-full px-6"> | ||
| <Link href="/onboarding/tutor/subjects">Back</Link> | ||
| </Button> | ||
| <Button variant="brand" className="rounded-full px-8 py-6 text-black" onClick={() => router.push("/dashboard")}> | ||
| Finish | ||
| <Button | ||
| variant="brand" | ||
| className="rounded-full px-8 py-6 text-black" | ||
| onClick={handleFinish} | ||
| disabled={isPending} | ||
| > | ||
| {isPending ? "Finishing..." : "Finish"} | ||
| </Button> | ||
| </div> | ||
| </main> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,13 +7,16 @@ import { broadcastToTutors, notifyStudent } from "@/server/wsBridge"; | |
|
|
||
| export const signalRouter = createTRPCRouter({ | ||
| // Returns the current viewer's role for conditional UI rendering | ||
| getViewerRole: protectedProcedure.query(async () => { | ||
| // Clerk manages auth, so default to STUDENT role | ||
| // Tutors will be explicitly set in DB when they complete tutor onboarding | ||
| return "STUDENT"; | ||
| }), | ||
| getViewerRole: protectedProcedure.query(async ({ ctx }) => { | ||
| // Find user by Clerk ID and check if they have a tutor profile | ||
| const user = await ctx.db.user.findUnique({ | ||
| where: { clerkUid: ctx.user.id }, | ||
| select: { id: true, role: true }, | ||
| }); | ||
|
|
||
| createSignal: protectedProcedure | ||
| // If user exists in DB, use their role; otherwise default to STUDENT | ||
| return user?.role ?? "STUDENT"; | ||
| }), createSignal: protectedProcedure | ||
|
Comment on lines
+10
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainFix formatting on line 19 and verify user existence handling. Line 19 has a formatting issue where the closing parenthesis, comma, and next procedure declaration are on the same line. Apply this diff to fix the formatting: // If user exists in DB, use their role; otherwise default to STUDENT
return user?.role ?? "STUDENT";
- }), createSignal: protectedProcedure
+ }),
+
+ createSignal: protectedProcedureAdditional observation: The fallback to Length of output: 34 🏁 Script executed: #!/bin/bash
# Look at the full context around lines 10-19 and beyond
if [ -f "src/server/api/routers/signal.ts" ]; then
echo "=== Lines 1-30 of signal.ts ===" && \
sed -n '1,30p' src/server/api/routers/signal.ts && \
echo "" && \
echo "=== Lines 110-135 (for acceptSignal and TODO context) ===" && \
sed -n '110,135p' src/server/api/routers/signal.ts
fiLength of output: 2623 🏁 Script executed: #!/bin/bash
# Search for TODO comments in signal.ts related to transactions/concurrency
if [ -f "src/server/api/routers/signal.ts" ]; then
rg -n "TODO|FIXME|transact|concur" src/server/api/routers/signal.ts -A2 -B2
fiLength of output: 291 🏁 Script executed: #!/bin/bash
# Search for Clerk webhook handlers and user sync logic
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" \) | xargs rg -l "clerk.*webhook|webhook.*user|user.*sync" 2>/dev/null | head -10Length of output: 1275 🏁 Script executed: #!/bin/bash
# Check for user creation/sync logic in server directory
rg -n "user.*create|onCreate.*user|webhook" src/server --type ts -A3 -B1 2>/dev/null | head -50Length of output: 0 🏁 Script executed: #!/bin/bash
# Verify the database schema for the user model
find . -type f \( -name "schema.prisma" -o -name "*schema*" \) | grep -E "(prisma|schema)" | head -5Length of output: 263 Length of output: 764 🏁 Script executed: #!/bin/bash
# Search for Clerk webhook handlers in API routes (next.js app structure)
find . -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/api/*" | xargs rg -l "clerk" 2>/dev/null | grep -v node_modules | head -10Length of output: 162 🏁 Script executed: #!/bin/bash
# Look for user creation in entire src directory with more context
rg -n "\.user\.create|\.user\.upsert" src/ --type ts -A 2 -B 2 2>/dev/null | head -40Length of output: 789 🏁 Script executed: #!/bin/bash
# Search for any webhook or sync logic files
find src -type f -name "*.ts" | xargs rg -l "webhook|sync|clerk" 2>/dev/nullLength of output: 292 🏁 Script executed: #!/bin/bash
# Check for middleware that might handle Clerk authentication setup
find src -type f -name "*.ts" | xargs rg -l "middleware|auth|clerk" 2>/dev/null | head -10Length of output: 220 Length of output: 1996 🏁 Script executed: #!/bin/bash
# Get full acceptSignal implementation context around line 111
sed -n '105,130p' src/server/api/routers/signal.tsLength of output: 1202 🏁 Script executed: #!/bin/bash
# Check if ctx.user is a Clerk user or application user
rg -n "ctx\.user" src/server/api/trpc.ts -B 3 -A 3 | head -50Length of output: 350 Fix formatting on line 19 and correct the user lookup in acceptSignal. Line 19 has a formatting issue where the closing parenthesis, comma, and next procedure are on the same line. Additionally, Apply these diffs: Formatting fix (line 19): return user?.role ?? "STUDENT";
- }), createSignal: protectedProcedure
+ }),
+
+ createSignal: protectedProcedureCritical bug fix in acceptSignal (line 111): // Guard: Only tutors can accept signals - check DB role
const me = await ctx.db.user.findUnique({
- where: { id: ctx.user.id },
+ where: { clerkUid: ctx.user.id },
select: { id: true, role: true },
});Additional observation: Line 128 documents a known TODO about transactional handling and concurrency locks. This should be addressed separately to prevent race conditions on signal acceptance. 🤖 Prompt for AI Agents |
||
| .input(z.object({ | ||
| message: z.string().min(1), | ||
| subject: z.string().min(1), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,11 @@ import { TRPCError } from "@trpc/server"; | |
| export const tutorRouter = createTRPCRouter({ | ||
| createProfile: protectedProcedure | ||
| .input(z.object({ | ||
| subjectInterests: z.array(z.string()), | ||
| teachingLevels: z.array(z.string()), | ||
| yearsOfExperience: z.number(), | ||
| teachingStyle: z.array(z.string()), | ||
| preferredSessionTypes: z.array(z.string()), | ||
| subjectInterests: z.array(z.string()).min(1, "At least one subject is required"), | ||
| teachingLevels: z.array(z.string()).min(1, "At least one teaching level is required"), | ||
| yearsOfExperience: z.number().min(0, "Years of experience cannot be negative"), | ||
| teachingStyle: z.array(z.string()).min(1, "At least one teaching style is required"), | ||
| preferredSessionTypes: z.array(z.string()).min(1, "At least one session type is required"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Line 14 requires Apply this diff to save the field: const newProfile = await tx.tutorProfile.create({
data: {
userId: user.id,
subjectInterests: input.subjectInterests,
teachingLevels: input.teachingLevels,
yearsOfExperience: input.yearsOfExperience,
teachingStyle: input.teachingStyle,
+ preferredSessionTypes: input.preferredSessionTypes,
},
});Consider also including Also applies to: 45-53 🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't think this is accurate @coderabbitai There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 2963 @AJ-505 You're absolutely right, and I apologize for the confusion. After checking the Prisma schema, I can see that the
The actual issue is a mismatch between the validation schema and the database schema:
This means either:
Which approach would you prefer? If 🧬 Code Graph Analysis Results
|
||
| })) | ||
| .mutation(async ({ ctx, input }) => { | ||
| const clerkUser = ctx.user; | ||
|
|
@@ -49,6 +49,7 @@ export const tutorRouter = createTRPCRouter({ | |
| teachingLevels: input.teachingLevels, | ||
| yearsOfExperience: input.yearsOfExperience, | ||
| teachingStyle: input.teachingStyle, | ||
| preferredSessionTypes: input.preferredSessionTypes, | ||
| }, | ||
| }); | ||
|
|
||
|
|
@@ -64,7 +65,7 @@ export const tutorRouter = createTRPCRouter({ | |
|
|
||
| const { embedding } = await invokeModel(embeddingText); | ||
|
|
||
| await ctx.db.tutorProfile.update({ where: { id: newProfile.id }, data: { embedding }}); | ||
| await ctx.db.tutorProfile.update({ where: { id: newProfile.id }, data: { embedding } }); | ||
|
|
||
| return newProfile; | ||
| }); | ||
|
|
@@ -121,7 +122,7 @@ ${updatedProfile.teachingLevels.map((l) => `- ${l}`).join("\n")} | |
| Teaching Style: | ||
| ${updatedProfile.teachingStyle.map((t) => `- ${t}`).join("\n")} | ||
| `); | ||
| await ctx.db.tutorProfile.update({ where: { id: updatedProfile.id}, data: { embedding}}); | ||
| await ctx.db.tutorProfile.update({ where: { id: updatedProfile.id }, data: { embedding } }); | ||
|
|
||
| return { success: true, message: "Tutor profile updated successfully", profile: updatedProfile }; | ||
| }), | ||
|
|
||
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.
🧩 Analysis chain
Verify application code handles nullable
preferredSessionTypescolumn.The migration adds
preferredSessionTypesas a nullable TEXT array with no DEFAULT value. Existing TutorProfile records will have NULL for this column upon migration completion. This can cause runtime errors if the application assumes the field always has a value.Please verify that:
preferredSessionTypes'{}'for an empty array) would be more appropriate to prevent NULL handling logic throughout the codebaseRun the following script to check how the field is referenced in application code:
Additionally, regarding the "P1017 Server has closed the connection" error mentioned in the PR comments: This appears to be a database connection issue rather than a SQL syntax error. Verify that your database server is running and the connection string is correct before re-running the migration.
🏁 Script executed:
Length of output: 8178
🏁 Script executed:
Length of output: 2920
🏁 Script executed:
Length of output: 22
🏁 Script executed:
Length of output: 1067
Fix schema-migration mismatch:
preferredSessionTypesmust match nullability across both files.The migration creates a nullable column (
TEXT[]withoutNOT NULL), but the Prisma schema defines it as required (String[]without?). This creates a data type mismatch: existing TutorProfile records will haveNULLvalues that conflict with the non-nullable TypeScript type, causing runtime errors when code likeArray.from(preferredSessionTypes)attempts to processNULL.Choose one fix:
DEFAULT '{}'::text[]andNOT NULLconstraint to match schema requirement, ORpreferredSessionTypes String[]topreferredSessionTypes String[]?Option 1 is recommended to maintain the application's expectation of always having an array.
🤖 Prompt for AI Agents