Codex/redesign core product pages for saas#21
Conversation
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughReplaces NextAuth with Clerk across auth, API routes, pages, middleware, and components; removes several NextAuth routes and auth helpers; refactors Doctor Green integration and DrGreen client; introduces Clerk webhook handler and many Clerk/seed scripts; performs large Prisma schema changes and UI adjustments. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Middleware
participant Clerk
participant API
participant Database
Client->>Middleware: HTTP request (Clerk auth headers)
Middleware->>Clerk: Validate/auth via clerkMiddleware
Clerk-->>Middleware: Auth result (user or null)
alt Auth valid
Middleware->>API: Forward request + x-tenant-* headers
API->>Clerk: getCurrentUser() / currentUser()
Clerk-->>API: Clerk user (id, email, publicMetadata)
API->>Database: Lookup/upsert local user by email or tenantId
Database-->>API: user/result
API-->>Client: 200/201 response
else Auth invalid
Middleware-->>Client: 401 Unauthorized
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
✏️ Tip: You can disable this entire section by setting Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
nextjs_space/app/api/super-admin/audit-logs/route.ts (1)
18-26: Add input validation for pagination parameters.
parseIntcan returnNaNfor invalid input, which would propagate to Prisma'stakeandskipoptions and cause unexpected behavior. Consider validating and clamping the values.Suggested fix
const { searchParams } = new URL(req.url); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "50"); + const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1); + const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "50", 10) || 50));nextjs_space/components/shop/ClientOnboarding.tsx (1)
92-103: Email default value won't populate due to async Clerk loading.
useForm'sdefaultValuesare evaluated once at initialization. Since Clerk loads asynchronously,useris likelyundefinedon first render, resulting in an empty email field that never updates even afteruserbecomes available.Additionally,
isLoadedis destructured but unused.🔧 Proposed fix using useEffect to reset form when user loads
const { user, isLoaded, isSignedIn } = useUser(); const personalForm = useForm<PersonalDetails>({ resolver: zodResolver(personalDetailsSchema), defaultValues: formData.personal || { firstName: "", lastName: "", - email: user?.primaryEmailAddress?.emailAddress || "", + email: "", phone: "", dateOfBirth: "", }, }); + + // Populate email when Clerk user data becomes available + useEffect(() => { + if (isLoaded && user?.primaryEmailAddress?.emailAddress && !formData.personal?.email) { + personalForm.setValue("email", user.primaryEmailAddress.emailAddress); + } + }, [isLoaded, user, personalForm, formData.personal?.email]);Alternatively, consider showing a loading skeleton until
isLoadedistrueto ensure user data is available before rendering the form.nextjs_space/app/api/super-admin/tenants/route.ts (1)
121-133: RemoveadminPasswordfrom the API contract; it's extracted, required, but discarded.You're collecting a password and then replacing it with a generated placeholder, which is a sensitive-data anti-pattern and misleads clients about the API contract. Since the code comment explicitly states Clerk handles authentication, remove
adminPasswordfrom the destructuring, validation check, and required fields.🛠️ Suggested fix
const { businessName, subdomain, countryCode, adminEmail, adminFirstName, adminLastName, - adminPassword, } = body; - if (!businessName || !subdomain || !adminEmail || !adminPassword) { + if (!businessName || !subdomain || !adminEmail) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 }, ); }nextjs_space/app/tenant-admin/audit-logs/page.tsx (1)
52-66: AddisLoadedandisSignedInguards before fetching audit logs.The
useEffectat line 64 callsfetchLogs()immediately without waiting for Clerk'suseUser()to finish loading. Since user loading is async, the API request fires before authentication is ready, causing premature 401 responses or false empty states. Per Clerk's documented best practices, always checkisLoadedfirst, thenisSignedIn, before making authenticated API calls.♻️ Required fix
- const { user } = useUser(); + const { user, isLoaded, isSignedIn } = useUser(); const [logs, setLogs] = useState<AuditLog[]>([]); const [pagination, setPagination] = useState<PaginationInfo>({ page: 1, limit: 50, total: 0, totalPages: 0, }); const [loading, setLoading] = useState(true); const [actionFilter, setActionFilter] = useState<string>(""); const [entityFilter, setEntityFilter] = useState<string>(""); useEffect(() => { + if (!isLoaded) return; + if (!isSignedIn) { + setLogs([]); + setLoading(false); + return; + } fetchLogs(); - }, [pagination.page, actionFilter, entityFilter]); + }, [isLoaded, isSignedIn, pagination.page, actionFilter, entityFilter]);nextjs_space/app/tenant-admin/page.tsx (1)
28-50: Guard missing email to prevent Prisma from selecting an arbitrary user.When
undefined, Prisma drops the filter and can return the first user, which can accidentally bind the wrong tenant. Fail fast if no email is present.🔧 Suggested guard
- const email = user.emailAddresses[0]?.emailAddress; + const email = + user.primaryEmailAddress?.emailAddress ?? + user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect("/sign-in"); + } const localUser = await prisma.users.findFirst({ - where: { email: email }, + where: { email }, include: { tenants: {nextjs_space/app/api/store/[slug]/orders/submit/route.ts (1)
51-70: Add email validation before passing to webhook.The
user.emailfromgetCurrentUser()uses optional chaining (user.emailAddresses[0]?.emailAddress), so it could beundefined. Currently, this potentially undefined value is passed directly to the webhook payload without validation. Webhook consumers may fail or misbehave if they expect a valid email. Validate thatuser.emailexists before thetriggerWebhookcall, or handle the case where it's missing.nextjs_space/app/api/tenant-admin/orders/[id]/admin-notes/route.ts (1)
15-42: Guard against missing Clerk email before tenant lookup.If
user.emailAddresses[0]is undefined, Prisma can end up querying without an email filter, risking cross-tenant access or a 500. Fail fast when email is missing.🛠️ Proposed fix
- const email = user.emailAddresses[0]?.emailAddress; + const email = user.emailAddresses[0]?.emailAddress; + if (!email) { + return NextResponse.json({ error: "Email required" }, { status: 400 }); + } const role = (user.publicMetadata.role as string) || ""; @@ - const localUser = await prisma.users.findFirst({ - where: { email: email }, + const localUser = await prisma.users.findFirst({ + where: { email }, include: { tenants: true }, });nextjs_space/app/api/tenant-admin/posts/route.ts (1)
62-71: FixauthorIdto use the local database user ID, not the Clerk user ID.
authorIdis being set touser.idfromgetCurrentUser(), which returns the raw Clerk user ID (format:user_xxxxx). However, theposts.authorIdcolumn is a foreign key to the localuserstable. The database user record'sidfield depends on how users are synced:
- The Clerk webhook (line 107 in
app/api/webhooks/clerk/route.ts) creates users withid: user_${clerkId}, resulting in double-prefixed IDs likeuser_user_xxxxx- The seed script uses
id: clerkUser.iddirectly, resulting inuser_xxxxxThis inconsistency will cause FK constraint violations when inserting posts. Either:
- Standardize user ID creation across all paths (webhook and seed) to use raw Clerk IDs, OR
- Look up the local database user and use their ID instead of the Clerk ID
nextjs_space/app/tenant-admin/branding/page.tsx (1)
15-25: Handle potential undefined email before database query.If
user.emailAddresses[0]is undefined,email: undefined, which may not behave as expected. Add an early guard for missing email.Proposed fix
const email = user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect('/sign-in'); + } + const localUser = await prisma.users.findFirst({ where: { email: email },nextjs_space/app/api/tenant-admin/posts/[id]/route.ts (1)
117-129: Potential null pointer error for SUPER_ADMIN without local user.Line 121 uses
localUser!.tenantIdwith a non-null assertion, but a SUPER_ADMIN bypasses the tenant ownership check at line 103 and may not have a corresponding local user record. This would cause a runtime error when checking slug uniqueness.Proposed fix
if (validatedData.title && validatedData.title !== existingPost.title) { const baseSlug = slugify(validatedData.title); let uniqueSlug = baseSlug; let counter = 1; + const tenantIdForSlug = localUser?.tenantId ?? existingPost.tenantId; while ( await prisma.posts.findFirst({ where: { slug: uniqueSlug, - tenantId: localUser!.tenantId, + tenantId: tenantIdForSlug, NOT: { id }, }, }) ) { uniqueSlug = `${baseSlug}-${counter}`; counter += 1; } dataToUpdate.slug = uniqueSlug; }nextjs_space/app/api/tenant-admin/orders/route.ts (1)
2-39: Tenant ID mismatch: Clerk Org ID stored in metadata, but local UUID used in database.The onboarding flow (
/api/onboarding/route.tsline 151) stores the Clerk Org ID (formatorg_...) topublicMetadata.tenantId, but creates a local tenant with a UUID in the database (line 199). WhengetCurrentUser()retrieves this metadata and passes it to database queries (orders/route.ts lines 35, 363), the mismatch causes all queries to fail silently with 404s or empty results.Fix: Update line 151 in
/api/onboarding/route.tsto store the localtenantId(UUID) instead ofclerkOrg.id:tenantId: tenantId // Use the local UUID, not clerkOrg.idThis applies to both GET and PATCH handlers as noted.
nextjs_space/app/api/onboarding/route.ts (1)
146-215: Move Clerk metadata update after local tenant creation and use local UUID.The code sets
publicMetadata.tenantIdtoclerkOrg.id(format:org_...), but all downstream routes extract this value and use it to query the database where the tenant ID is a UUID. This causes database lookups to fail. Update metadata after creating the local tenant with the correct UUID.✅ Align metadata with local tenant ID
- // 4. Update Clerk User Metadata with new Org ID (for easier lookup later) - const client = await clerkClient(); - await client.users.updateUserMetadata(clerkUser.id, { - publicMetadata: { - role: "TENANT_ADMIN", - tenantId: clerkOrg.id - } - }); ... - const tenant = await prisma.tenants.create({ + const tenant = await prisma.tenants.create({ data: { id: tenantId, ... settings: { contactInfo, templatePreset: templateId || "modern", clerkOrgId: clerkOrg.id }, }, }); + // 4. Update Clerk User Metadata with local Tenant UUID + const client = await clerkClient(); + await client.users.updateUserMetadata(clerkUser.id, { + publicMetadata: { + role: "TENANT_ADMIN", + tenantId: tenant.id, + clerkOrgId: clerkOrg.id + } + });nextjs_space/lib/doctor-green-api.ts (1)
84-88: Dead code: redundant apiKey check.Line 88 (
if (!apiKey) console.warn(...)) will never execute because lines 84-86 already throw an error if!apiKey || !secretKey. This warning is unreachable.🧹 Proposed fix
if (!apiKey || !secretKey) { throw new Error("MISSING_CREDENTIALS"); } - if (!apiKey) console.warn("Warning: No Dr Green API Key provided"); - return callDrGreenAPI(endpoint, {nextjs_space/app/api/tenant-admin/settings/route.ts (1)
89-105: Critical: Duplicate encryption ofdrGreenApiKey.The same encryption logic for
drGreenApiKeyappears twice (lines 89-96 and 98-105). This will encrypt the key twice if provided, resulting in corrupted/unusable data.🐛 Proposed fix: Remove duplicate block
if (drGreenApiKey && drGreenApiKey.trim() !== '') { try { dataToUpdate.drGreenApiKey = encrypt(drGreenApiKey); } catch (e) { console.error('Encryption failed:', e); throw e; } } - - if (drGreenApiKey && drGreenApiKey.trim() !== '') { - try { - dataToUpdate.drGreenApiKey = encrypt(drGreenApiKey); - } catch (e) { - console.error('Encryption failed:', e); - throw e; - } - }nextjs_space/templates/healingbuds/components/Navigation.tsx (1)
253-309: Replace stalestatuschecks with Clerk auth state.
statusis undefined in this component, so rendering at Line 253/399 will throw. UseisLoaded && isSignedInfrom Clerk instead.🛠️ Proposed fix
- {status === 'authenticated' ? ( + {isLoaded && isSignedIn ? ( <DropdownMenu> ... </DropdownMenu> ) : ( <Link href={`/store/${tenant.subdomain}/login`} ... > <User className="w-6 h-6" /> </Link> )}- {status === 'authenticated' ? ( + {isLoaded && isSignedIn ? ( <> ... </> ) : ( <Link href={`/store/${tenant.subdomain}/login`} ... > Patient Login </Link> )}Also applies to: 399-432
🤖 Fix all issues with AI agents
In `@nextjs_space/app/api/store/`[slug]/cart/add/route.ts:
- Around line 12-16: The route is passing Clerk's user.id from getCurrentUser()
into addToCart/getCart causing ensureClientId() to fail — switch to using
currentUser(), lookup the application DB user by email (as done in
cart/clear/route.ts), and pass the DB user's id (dbUser.id) into
addToCart/getCart; update both this file (route.ts under cart/add) and
cart/route.ts to replace getCurrentUser() usage with currentUser(), perform
prisma.users.findUnique({ where: { email: user.email } }) (or equivalent) to
obtain dbUser, and forward dbUser.id to ensureClientId()/cart functions.
In `@nextjs_space/app/api/store/`[slug]/orders/[orderId]/route.ts:
- Around line 12-24: The code mixes Clerk ID and local DB user IDs causing
inconsistent lookups; standardize to lookup users by email across order routes:
get the current user via currentUser(), extract email from
user.emailAddresses[0].emailAddress, then always use prisma.users.findFirst({
where: { email } }) to obtain dbUser and use dbUser.id for all order queries
(including in GET /api/store/[slug]/orders, POST /api/store/[slug]/orders/submit
and POST /api/orders) or alternatively ensure every user creation path writes
the Clerk ID into the users table (e.g., clerkId field) and change all queries
to use that clerkId consistently; update references to currentUser(),
emailAddresses, prisma.users.findFirst, dbUser and submitOrder() accordingly so
all routes use the same lookup key.
In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts:
- Around line 22-32: The request lacks a guard for missing email which lets
prisma.users.findFirst({ where: { email } }) run with undefined and match
arbitrary users; add an explicit check for user.emailAddresses[0]?.emailAddress
(the local variable email) and return a 401/400 before calling
prisma.users.findFirst in this route handler and the corresponding GET, PATCH,
DELETE handlers in route.ts; ensure the same guard is applied wherever you use
prisma.users.findFirst with email to prevent cross-tenant access and only
proceed when email is a non-empty string.
In `@nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts`:
- Around line 15-24: The extracted email variable (const email =
user.emailAddresses[0]?.emailAddress) can be undefined and must be explicitly
guarded before calling prisma.users.findFirst; add a check immediately after
that line to return an error response (401 or 400) if email is falsy, so you
don't call prisma.users.findFirst({ where: { email } }) with undefined which
causes unintended user matches—update the route handler to validate email and
only proceed to the prisma.users.findFirst call when email is present.
In `@nextjs_space/app/api/tenant-admin/webhooks/`[id]/route.ts:
- Around line 22-35: The Prisma lookup uses email which may be undefined and
will remove the filter in Prisma 6.7.0; add an explicit guard that verifies
email (from user.emailAddresses[0]?.emailAddress) is a non-empty string before
calling prisma.users.findFirst in the PATCH handler (route.ts) and return an
error (e.g., 400/401 JSON) if missing, then apply the identical guard to the
DELETE endpoint; update the logic around the localUser/tenantId fetching so
prisma.users.findFirst only runs when email is truthy to avoid accidental
full-table matches.
In `@nextjs_space/app/api/user/profile/route.ts`:
- Around line 45-54: The Prisma update call in prisma.users.update is using the
non-null-asserted user.email (user.email!) which can be undefined at runtime;
change the query to use the stable user.id instead. Locate the
prisma.users.update block in route.ts (the code that builds the where: { ... }
object) and replace where: { email: user.email! } with where: { id: user.id } so
the update uses the always-present Clerk user id; keep the rest of the data
object unchanged.
In `@nextjs_space/app/store/`[slug]/the-wire/[postSlug]/page.tsx:
- Line 6: Restore HTML sanitization for rendered post content: re-enable the
DOMPurify import (DOMPurify from "isomorphic-dompurify") and apply it to
post.content before passing it into the renderer (where dangerouslySetInnerHTML
or similar is used in the page component that renders post.content). Ensure you
call DOMPurify.sanitize(post.content, {SAFE_FOR_TEMPLATES: true} or equivalent
safe config) and use the sanitized string in place of raw post.content so stored
XSS is prevented; update any variable names around post.content rendering to use
the sanitized value.
In `@nextjs_space/app/tenant-admin/seo/page.tsx`:
- Around line 18-24: Guard against undefined email by reading
user.primaryEmailAddress (or validate user.emailAddresses[0]) and
returning/redirecting or throwing if it's missing before calling the DB; replace
the prisma.users.findFirst({ where: { email } }) call with
prisma.users.findUnique({ where: { email } }) using the validated email
variable, and keep using the localUser.tenantId check after the safe lookup.
Apply the same change pattern wherever tenant-admin pages use
user.emailAddresses[0] and findFirst to avoid leaking another tenant's data.
In `@nextjs_space/scripts/seed-clerk-orgs.ts`:
- Around line 93-110: The loop that calls
clerkClient.organizations.createOrganizationMembership is passing the Prisma
primary key (user.id) instead of the Clerk user ID; change the call to use the
Clerk user ID field stored on your user record (e.g., user.clerkId or
user.clerkUserId) when setting userId in createOrganizationMembership, and add a
guard to log and skip users missing that Clerk ID so you don't attempt
membership creation with an invalid identifier; update references in the
tenant.users iteration and the error/log messages accordingly.
- Around line 51-53: The seed uses Prisma user IDs (tenant.users[0]?.id,
adminId, userId) where Clerk expects Clerk user IDs (e.g.,
createOrganization.createdBy); fix by resolving the Clerk ID before calling
Clerk APIs: for each user referenced (tenant.users[0], adminId, userId) query
your users table for a clerkUserId column or lookup the Clerk user by email (via
Clerk SDK) and substitute that clerkUserId into createdBy/adminId/userId; update
the seed-clerk-orgs logic (the block calling createOrganization and where
adminId/userId are assigned) to use the resolved Clerk user IDs instead of
Prisma UUIDs.
🟠 Major comments (28)
nextjs_space/check-user.ts-1-1 (1)
1-1: Debug script needs refactoring before merge.Several concerns with this utility script:
Code duplication: This file is nearly identical to
check-user-pt.ts, differing only by email address. Consider consolidating into a single parameterized script.Hardcoded PII: Email address is hardcoded in source. Accept it as a CLI argument instead.
No error handling: Unhandled promise rejection if query fails.
Formatting: Single-line code is unreadable.
Location: Other debug scripts are in
scripts/directory (e.g.,debug-drgreen-raw.ts,seed-co-admin.ts).♻️ Suggested consolidated script at `scripts/check-user.ts`
import { prisma } from '../lib/db'; async function checkUser(email: string) { try { const user = await prisma.users.findUnique({ where: { email }, include: { tenants: true }, }); if (!user) { console.log(`No user found with email: ${email}`); return; } console.log(JSON.stringify(user, null, 2)); } catch (error) { console.error('Failed to fetch user:', error); process.exit(1); } finally { await prisma.$disconnect(); } } const email = process.argv[2]; if (!email) { console.error('Usage: npx ts-node scripts/check-user.ts <email>'); process.exit(1); } checkUser(email);Usage:
npx ts-node scripts/check-user.ts admin@healingbuds.conextjs_space/check-user-pt.ts-1-1 (1)
1-1: Remove duplicate script.This file is a copy of
check-user.tswith only the email address changed. Delete this file and use the consolidated parameterized script suggested in the other review comment.nextjs_space/scripts/seed-real-users.ts-5-18 (1)
5-18: Add environment guard and use stronger seed credentials.This script seeds admin users with a weak hardcoded password. While this appears to be a development/staging seed script, consider:
- Adding an environment check to prevent accidental execution in production
- Using a stronger default password or environment variable
🔒 Proposed fix
async function main() { + // Prevent accidental execution in production + if (process.env.NODE_ENV === 'production') { + console.error('This seed script should not run in production!'); + process.exit(1); + } + const tenantEmail = 'gerard@automatos.app'; const superEmail = 'gerard161@gmail.com'; - const password = await hash('admin123', 12); + const password = await hash(process.env.SEED_PASSWORD || 'DevAdmin123!', 12);nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx-32-36 (1)
32-36: Handle missing email address to avoid undefined query behavior.If the Clerk user has no verified email addresses,
undefined, and querying Prisma with{ email: undefined }may return unexpected results (e.g., a user with a NULL email).Consider adding an explicit check:
🛡️ Proposed fix
// Verify tenant access const email = user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect("/sign-in"); + } const localUser = await prisma.users.findFirst({ where: { email: email }, include: { tenants: true }, });nextjs_space/scripts/seed-co-admin.ts-5-8 (1)
5-8: Remove hardcoded credentials; load from env and require a strong secret.Embedding a plaintext password in the repo is a security/compliance risk and encourages weak credentials in real environments. Prefer reading from env and fail fast if missing.
🔐 Proposed fix
async function main() { - const email = 'admin@healingbuds.co'; - const password = await hash('admin123', 12); + const email = process.env.SEED_ADMIN_EMAIL ?? 'admin@healingbuds.co'; + const rawPassword = process.env.SEED_ADMIN_PASSWORD; + if (!rawPassword) { + throw new Error('SEED_ADMIN_PASSWORD is required'); + } + const password = await hash(rawPassword, 12);nextjs_space/scripts/debug_user.ts-21-22 (1)
21-22: Avoid dumping full user records (PII) to logs.
JSON.stringify(users, null, 2)can expose sensitive fields in shared logs. Consider logging only a minimal, redacted view.🔐 Safer logging
- console.log("Found Users:", JSON.stringify(users, null, 2)); + const redacted = users.map((u: any) => ({ + id: u.id, + email: u.email?.replace(/(^.).+(@.*$)/, "$1***$2"), + tenantId: u.tenantId + })); + console.log("Found Users:", JSON.stringify(redacted, null, 2));nextjs_space/scripts/debug_user.ts-21-32 (1)
21-32: Avoid asyncforEach— pending queries will be dropped before Prisma disconnect.
forEachdoesn't await the async callback, so afterforEachcompletes,main()immediately returns and thefinallyblock disconnects Prisma while tenant queries (line 25) are still pending. Use afor...ofloop to ensure all queries complete before disconnect.✅ Safer loop
- users.forEach(async (user: any) => { - if (user.tenantId) { - const tenant = await prisma.tenants.findUnique({ - where: { id: user.tenantId } - }); - console.log(`User ${user.email} associated with tenant:`, tenant?.businessName); - } else { - console.log(`User ${user.email} has NO tenantId set.`); - } - }); + for (const user of users) { + if (user.tenantId) { + const tenant = await prisma.tenants.findUnique({ + where: { id: user.tenantId } + }); + console.log(`User ${user.email} associated with tenant:`, tenant?.businessName); + } else { + console.log(`User ${user.email} has NO tenantId set.`); + } + }nextjs_space/scripts/fix_missing_user.ts-7-11 (1)
7-11: Avoid committing PII/tenant IDs and logging full emails.Hardcoding real emails/tenant IDs in the repo and logging them verbatim is a compliance/privacy risk. Prefer env/CLI inputs and redact logs.
🔧 Suggested change (env/argv + redacted log)
async function main() { - const email = "gerard161+clerk@gmail.com"; - const tenantId = "1ae9473f-318d-4c5b-9f1c-e43caf7986cd"; - const businessName = "clerk"; + const email = process.env.FIX_USER_EMAIL ?? process.argv[2]; + const tenantId = process.env.FIX_USER_TENANT_ID ?? process.argv[3]; + const businessName = process.env.FIX_USER_BUSINESS_NAME ?? "clerk"; + if (!email || !tenantId) { + console.error("Missing email or tenantId. Set FIX_USER_EMAIL/FIX_USER_TENANT_ID or pass argv."); + process.exit(1); + } + const redactedEmail = email.replace(/(^.).*(@.*$)/, "$1***$2"); - console.log(`Restoring missing user: ${email} for tenant: ${tenantId}`); + console.log(`Restoring missing user: ${redactedEmail} for tenant: ${tenantId}`);nextjs_space/scripts/fix_missing_user.ts-13-37 (1)
13-37: Useupsertand avoid storing plaintext password placeholders.The check-then-create pattern has a race condition window. More importantly, storing the literal string
CLERK_MANAGED_ACCOUNTas a plaintext password is a security concern. The schema requirespasswordto be a non-nullableString, so usebcryptjs.hash()(already in your codebase—seeseed.ts,seed-real-users.ts) or a hashed placeholder value like inseed-clerk-users.ts.Replace the check-then-update/create flow with
upsert, which is safe since♻️ Suggested change (upsert + hashed password)
- const existing = await prisma.users.findUnique({ - where: { email } - }); - - if (existing) { - console.log("User already exists! Updating tenantId..."); - await prisma.users.update({ - where: { email }, - data: { tenantId } - }); - } else { - console.log("Creating new user record..."); - await prisma.users.create({ - data: { - id: nodeCrypto.randomUUID(), - email, - password: "CLERK_MANAGED_ACCOUNT", - name: businessName, - role: "TENANT_ADMIN", - tenantId: tenantId, - updatedAt: new Date(), - } - }); - } + console.log("Upserting user record..."); + await prisma.users.upsert({ + where: { email }, + update: { tenantId }, + create: { + id: nodeCrypto.randomUUID(), + email, + // Use bcryptjs.hash() like seed files, or a hashed placeholder + password: await bcrypt.hash('clerk-managed-account', 10), + name: businessName, + role: "TENANT_ADMIN", + tenantId, + updatedAt: new Date(), + } + });nextjs_space/app/auth/callback/page.tsx-13-27 (1)
13-27: Verify admin role server‑side; do not route based on client‑readable metadata alone.
WhilepublicMetadatacannot be modified from client code, it is still readable by the frontend and should not be the sole basis for routing to protected admin areas. Implement server‑side verification using Clerk's session/auth context (orprivateMetadatavia backend API) before granting access to admin routes.nextjs_space/app/auth/callback/page.tsx-15-35 (1)
15-35: Guard email presence before DB lookup and prefer primaryEmailAddress.Clerk supports phone-only accounts without email addresses. The current code queries the database without verifying that
primaryEmailAddress(the recommended server-side pattern per Clerk documentation) with a fallback, and guard the DB lookup with an email presence check. If no role and no email exist, redirect to a safe fallback.🛠️ Suggested fix
- const email = user.emailAddresses[0]?.emailAddress; + const email = + user.primaryEmailAddress?.emailAddress ?? + user.emailAddresses[0]?.emailAddress; @@ - if (!role) { - const localUser = await prisma.users.findFirst({ where: { email } }); - if (localUser?.role === "SUPER_ADMIN") return redirect("/super-admin"); - if (localUser?.role === "TENANT_ADMIN") return redirect("/tenant-admin"); - } + if (!role && email) { + const localUser = await prisma.users.findFirst({ where: { email } }); + if (localUser?.role === "SUPER_ADMIN") return redirect("/super-admin"); + if (localUser?.role === "TENANT_ADMIN") return redirect("/tenant-admin"); + } + if (!role) { + return redirect("/"); + }nextjs_space/app/api/webhooks/clerk/route.ts-58-59 (1)
58-59: Avoid logging full webhook payload (PII).Line 59 logs the entire body, which includes emails and names. This is a privacy/compliance risk in production logs.
✅ Proposed fix (reduce logging)
- console.log("Webhook body:", body); + // Avoid logging full payload (PII). Keep minimal metadata only. + console.log("Webhook received.");nextjs_space/app/api/webhooks/clerk/route.ts-77-110 (1)
77-110: Make user sync idempotent with an atomic upsert.Lines 77-110 use
findUniquefollowed by conditionalupdate/create, which can race under concurrent webhook retries and throw unique constraint errors on theupsertto make this atomic while still avoiding overwrites ofroleandtenantId.Critical: The proposed fix uses
role: "CONSUMER", but the schema only allowsPATIENT,TENANT_ADMIN, orSUPER_ADMIN. Userole: "PATIENT"instead (the schema default).✅ Corrected atomic upsert
- const existingUser = await prisma.users.findUnique({ where: { email } }); - - if (existingUser) { - await prisma.users.update({ - where: { email }, - data: { - name: name || undefined, - firstName: first_name || undefined, - lastName: last_name || undefined, - updatedAt: new Date(), - // We do NOT update role or tenantId here typically, as that's business logic - } - }); - } else { - await prisma.users.create({ - data: { - email, - name: name, - firstName: first_name || null, - lastName: last_name || null, - role: "CONSUMER", // Default role - isActive: true, - id: `user_${id}`, // Sync IDs if possible or just use generated - updatedAt: new Date(), - } - }); - } + await prisma.users.upsert({ + where: { email }, + update: { + name: name || undefined, + firstName: first_name || undefined, + lastName: last_name || undefined, + updatedAt: new Date(), + // Do NOT update role or tenantId here + }, + create: { + email, + name, + firstName: first_name || null, + lastName: last_name || null, + role: "PATIENT", + isActive: true, + id: `user_${id}`, + updatedAt: new Date(), + }, + });nextjs_space/app/api/webhooks/clerk/route.ts-31-46 (1)
31-46: Use the raw request body for Svix signature verification.Lines 32–34 parse the JSON then stringify it. Svix signs the request body as raw bytes (svix-id + "." + svix-timestamp + "." + raw_body), so any re-encoding—including parse-then-stringify—changes the string and breaks signature verification. Call
req.text()once to get the raw body, then verify it directly.Proposed fix
- const payload = await req.json(); - const body = JSON.stringify(payload); + const body = await req.text();nextjs_space/app/api/signup/route.ts-3-3 (1)
3-3: Remove unused password validation and plaintext storageThe input
passwordis required in API validation (line 23) but never used—only a plaintextclerk_managed_<uuid>placeholder is stored (lines 59–66). Since authentication is fully handled by Clerk, the password field should be made nullable in the schema to reflect the Clerk-only architecture, and the validation check should be removed to eliminate the confusing dead code.Suggested fix
- if (!email || !password || !firstName || !lastName) { + if (!email || !firstName || !lastName) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 }, ); } - const placeholderPassword = `clerk_managed_${crypto.randomUUID()}`; const user = await prisma.users.create({ data: { email, - password: placeholderPassword, name: `${firstName} ${lastName}`, role: "PATIENT", tenantId: tenant.id, }, });Also requires updating the Prisma schema to make
passwordnullable:password String?Also applies to: 59–66
nextjs_space/app/api/tenant-admin/orders/[id]/route.ts-2-2 (1)
2-2: Inconsistent authentication pattern and missing role check.This file uses
currentUser()directly and performs a manual DB lookup fortenantId, whereas other tenant-admin routes (e.g.,products/reorder/route.ts) use thegetCurrentUser()helper from@/lib/auth-helperwhich extractstenantIddirectly frompublicMetadata.More critically, this endpoint lacks a role authorization check. Other tenant-admin routes verify the user has
TENANT_ADMINorSUPER_ADMINrole before proceeding. Without this check, any authenticated Clerk user could potentially access order details.Proposed fix
import { NextResponse } from "next/server"; -import { currentUser } from "@clerk/nextjs/server"; +import { getCurrentUser } from "@/lib/auth-helper"; import { prisma } from "@/lib/db"; /** * GET /api/tenant-admin/orders/[id] * * Fetch a single order by ID for the authenticated tenant admin. * Used for packing slip generation and order detail views. */ export async function GET( request: Request, { params }: { params: { id: string } }, ) { try { - const user = await currentUser(); + const user = await getCurrentUser(); - if (!user) { + if ( + !user || + (user.role !== "TENANT_ADMIN" && user.role !== "SUPER_ADMIN") + ) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const email = user.emailAddresses[0]?.emailAddress; - - // Fetch user's tenant - const localUser = await prisma.users.findFirst({ - where: { email: email }, - select: { tenantId: true }, - }); - - if (!localUser?.tenantId) { + if (!user.tenantId) { return NextResponse.json( { error: "No tenant associated with user" }, { status: 403 }, ); } // Fetch order with items and user data const order = await prisma.orders.findFirst({ where: { id: params.id, - tenantId: localUser.tenantId, + tenantId: user.tenantId, },Also applies to: 16-28
nextjs_space/app/api/super-admin/templates/[id]/route.ts-13-19 (1)
13-19: Guard email before audit log; verify Clerk role configuration is server-controlled.
email!at line 89 bypasses the optional chain from line 19—if a user lacks an email address, the audit log receivesundefinedand fails. Additionally,user.publicMetadata.roledepends entirely on your Clerk setup; if this metadata is ever modifiable by clients (through misconfiguration), it opens privilege escalation. Confirm with your Clerk admin that publicMetadata is set only server-side and cannot be tampered with by end users.🔧 Fix email handling
const email = user.emailAddresses[0]?.emailAddress; + + if (!email) { + return NextResponse.json( + { error: "User email missing" }, + { status: 400 }, + ); + } @@ - userEmail: email!, + userEmail: email,Also applies to: line 89
nextjs_space/app/tenant-admin/the-wire/page.tsx-20-27 (1)
20-27: Guard against unscoped user lookup when email is missing.If
user.emailAddresses[0]?.emailAddressis undefined, Prisma ignores the email filter and returns an arbitrary user record, exposing another tenant's posts. UseprimaryEmailAddressas a fallback and validate the email exists before querying.🔧 Suggested fix
const email = user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect("/sign-in"); + } const localUser = await prisma.users.findFirst({ - where: { email: email }, + where: { email }, include: { tenants: true }, });Alternatively, use
primaryEmailAddressas a fallback:- const email = user.emailAddresses[0]?.emailAddress; + const email = user.primaryEmailAddress?.emailAddress ?? user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect("/sign-in"); + }nextjs_space/app/tenant-admin/customers/[id]/page.tsx-24-32 (1)
24-32: Guard email and tenantId before querying customer data.If
undefined, Prisma may not filter correctly. Additionally,tenantIdcan beundefinedand the customer query on line 34 relies on optional chaining...(tenantId && { tenantId })to conditionally filter, allowing a request without proper tenant isolation if email lookup fails. While line 55-57 provides a post-query validation, fail fast with explicit guards.🔧 Suggested guard
let tenantId: string | undefined; if (user.publicMetadata.role === "TENANT_ADMIN") { - const email = user.emailAddresses[0]?.emailAddress; + const email = + user.primaryEmailAddress?.emailAddress ?? + user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect("/sign-in"); + } const localUser = await prisma.users.findFirst({ - where: { email: email }, + where: { email }, select: { tenantId: true }, }); + if (!localUser?.tenantId) { + notFound(); + } tenantId = localUser.tenantId; }nextjs_space/app/api/tenant-admin/tenant/route.ts-13-17 (1)
13-17: Add a role gate for this tenant‑admin endpoint.
Right now any authenticated user with a tenantId can access tenant data. Restrict to TENANT_ADMIN/SUPER_ADMIN to avoid exposure.✅ Suggested authorization check
- if (!user || !user.tenantId) { + if ( + !user || + !user.tenantId || + !["TENANT_ADMIN", "SUPER_ADMIN"].includes(user.role) + ) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }nextjs_space/app/tenant-admin/settings/page.tsx-18-33 (1)
18-33: Add email validation before the Prisma query to prevent undefined email lookups.
The email is extracted with optional chaining but never validated before being used as the lookup key for tenant data. If the email is undefined, the query behavior becomes unreliable and could fail to properly scope the tenant lookup.🛡️ Suggested guard
const email = user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect("/sign-in"); + } const localUser = await prisma.users.findFirst({ where: { email }, include: { tenants: true }, });nextjs_space/app/tenant-admin/emails/[id]/page.tsx-20-27 (1)
20-27: Guard missing email before the Prisma lookup to prevent cross‑tenant access.
Clerk'scurrentUser()can return a user without verified email addresses. IfemailAddresses[0]?.emailAddressis undefined, Prisma'sfindFirst({ where: { email: undefined } })removes the filter entirely, returning an unfiltered first row. The subsequent code then uses this wrong user's tenant ID to query templates, allowing access to unintended tenant data. Add an explicit email check and redirect before the database query.🛠️ Suggested hardening
const email = user.emailAddresses[0]?.emailAddress; + if (!email) { + redirect("/sign-in"); + } const localUser = await prisma.users.findFirst({ where: { email: email }, include: { tenants: true }, });nextjs_space/app/api/super-admin/platform-settings/route.ts-112-121 (1)
112-121: Keep GET authorization consistent with POST.GET only checks
user.role. If a super admin exists in the DB but their token metadata is missing, GET will return 403 while POST allows access. Align GET with the dual-check used in POST.🛠️ Proposed fix
- // Role check - if (user.role !== 'SUPER_ADMIN') { - // Optional: db check - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } + const dbUser = user.email + ? await prisma.users.findUnique({ where: { email: user.email } }) + : null; + if (dbUser?.role !== "SUPER_ADMIN" && user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + }nextjs_space/components/admin/HeaderProfile.tsx-25-27 (1)
25-27: Inconsistent sign-out redirect URL.The
signOutredirects to/auth/login, but other files in this PR (e.g.,branding/page.tsxline 12,super-admin/layout.tsxline 22) redirect unauthorized users to/sign-in. This inconsistency could cause confusion or 404 errors if/auth/loginno longer exists after the Clerk migration.Proposed fix
const handleLogout = async () => { - await signOut({ redirectUrl: "/auth/login" }); + await signOut({ redirectUrl: "/sign-in" }); };nextjs_space/scripts/seed-clerk-users.ts-17-37 (1)
17-37: Security: Weak hardcoded passwords for admin accounts.Using
admin123as passwords, even for dev/test seeding, creates security risks if this script is accidentally run in a non-development environment or if these credentials leak. Consider using environment variables or generating random passwords that are logged to console during seeding.Suggested approach
+const generateSecurePassword = () => { + return crypto.randomUUID().replace(/-/g, '').slice(0, 16); +}; + const USERS_TO_SEED = [ { email: 'admin@budstack.io', - password: 'admin123', + password: process.env.SEED_ADMIN_PASSWORD || generateSecurePassword(), firstName: 'Super', lastName: 'Admin', // ... }, // ... ];nextjs_space/app/api/tenant-admin/email-templates/[id]/route.ts-10-26 (1)
10-26: Inconsistent auth pattern: UsegetCurrentUser()helper instead of directcurrentUser()call.This file uses
currentUser()directly and performs an email-based database lookup to retrieve tenant info. However, other files in this PR (e.g.,email-mappings/route.ts,templates/[id]/activate/route.ts) use thegetCurrentUser()helper which already extractstenantIdfrom Clerk'spublicMetadata, avoiding the extra database query.For consistency and efficiency, consider aligning with the established pattern.
Suggested refactor for GET handler
import { NextRequest, NextResponse } from "next/server"; -import { currentUser } from "@clerk/nextjs/server"; +import { getCurrentUser } from "@/lib/auth-helper"; import { prisma } from "@/lib/db"; export async function GET( req: NextRequest, { params }: { params: { id: string } }, ) { try { - const clerkUser = await currentUser(); + const user = await getCurrentUser(); if ( - !clerkUser || - !["TENANT_ADMIN", "SUPER_ADMIN"].includes( - (clerkUser.publicMetadata.role as string) || "", - ) + !user || + !["TENANT_ADMIN", "SUPER_ADMIN"].includes(user.role || "") ) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const email = clerkUser.emailAddresses[0]?.emailAddress; - - const user = await prisma.users.findFirst({ - where: { email: email }, - include: { tenants: true }, - }); - - if (!user?.tenants) { - return NextResponse.json({ error: "Tenant not found" }, { status: 404 }); + const tenantId = user.tenantId; + if (!tenantId) { + return NextResponse.json({ error: "Tenant not found for user" }, { status: 404 }); } const template = await prisma.email_templates.findFirst({ where: { id: params.id, - tenantId: user.tenants.id, // Strict ownership + tenantId: tenantId, // Strict ownership }, });Apply the same pattern to PUT and DELETE handlers.
nextjs_space/app/api/tenant-admin/templates/[id]/activate/route.ts-6-28 (1)
6-28: Fix params type annotation to match Next.js 14.2.35.The
paramsparameter is incorrectly typed asPromise<{ id: string }>(line 8) and awaited on line 27. In Next.js 14.2.35,paramsis a synchronous object, not a Promise. The Promise-based params pattern is exclusive to Next.js 15.While awaiting a non-Promise value returns the value (so runtime behavior may work), the type annotation is incompatible with the project's Next.js version. Change line 8 to
{ params }: { params: { id: string } }and line 27 toconst { id } = params;.nextjs_space/app/api/tenant-admin/webhooks/[id]/deliveries/route.ts-21-34 (1)
21-34: Add email validation before the Prisma lookup to prevent data leaks.If
user.emailAddressesis empty (e.g., users authenticated via phone or username),undefined. Prisma omitsundefinedvalues from thewhereclause, causingfindFirst()to return any user record instead of filtering by email. This would allow the query to retrieve another tenant's data.Add an early guard before the Prisma query:
🛠️ Proposed fix
const email = user.emailAddresses[0]?.emailAddress; + if (!email) { + return NextResponse.json( + { error: "User email not available" }, + { status: 400 }, + ); + } const role = (user.publicMetadata.role as string) || ""; if (role !== "TENANT_ADMIN") { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const localUser = await prisma.users.findFirst({ - where: { email: email }, + where: { email }, select: { tenantId: true }, });
| const email = user.emailAddresses[0]?.emailAddress; | ||
| const role = (user.publicMetadata.role as string) || ""; | ||
|
|
||
| if (role !== "TENANT_ADMIN") { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const localUser = await prisma.users.findFirst({ | ||
| where: { email: email }, | ||
| select: { tenantId: true }, | ||
| }); | ||
|
|
||
| const tenantId = localUser?.tenantId; | ||
| if (!tenantId) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "route.ts" | grep -E "webhook|tenant-admin" | head -20Repository: AutomatosAI/budstack-saas
Length of output: 1248
🏁 Script executed:
git ls-files | grep -E "webhook.*route\.ts|tenant-admin.*route\.ts"Repository: AutomatosAI/budstack-saas
Length of output: 2048
🏁 Script executed:
fd "route.ts" --path "*tenant-admin*webhook*"Repository: AutomatosAI/budstack-saas
Length of output: 284
🏁 Script executed:
cat -n nextjs_space/app/api/tenant-admin/webhooks/[id]/route.tsRepository: AutomatosAI/budstack-saas
Length of output: 5633
🏁 Script executed:
rg "where.*undefined" nextjs_space --type ts -A 2 -B 2 | head -50Repository: AutomatosAI/budstack-saas
Length of output: 51
🌐 Web query:
Prisma undefined value in where clause filter behavior
💡 Result:
Short answer: by default Prisma treats undefined in a where value as “no filter” (the key is removed from the generated query). That means:
- Passing { field: undefined } is equivalent to not specifying field at all. [1]
- Operators behave differently when undefined entries are present: AND/NOT with undefined act like “no filter” (return all), but OR with undefined can produce an always-false condition (no results) in some cases. This has caused surprising behavior (see GitHub issue and StackOverflow thread). [2][3]
If you want safer behavior, enable Prisma’s strictUndefinedChecks (preview introduced in v5.20.0) which changes how undefined is handled to avoid accidental query removal. [1][4]
Sources:
- Prisma docs: Null and undefined in Prisma Client. [1]
- GitHub issue discussing empty objects / where producing WHERE 1=0. [2]
- StackOverflow discussion about OR/undefined returning nothing. [3]
- Prisma discussion/announcement for strictUndefinedChecks (v5.20.0). [4]
🏁 Script executed:
cat -n nextjs_space/package.json | grep -A 5 -B 5 prismaRepository: AutomatosAI/budstack-saas
Length of output: 1530
🏁 Script executed:
cat nextjs_space/prisma/schema.prisma | head -20Repository: AutomatosAI/budstack-saas
Length of output: 477
🏁 Script executed:
rg "emailAddresses" nextjs_space --type ts -B 3 -A 3 | head -60Repository: AutomatosAI/budstack-saas
Length of output: 4105
🌐 Web query:
Clerk currentUser() emailAddresses guaranteed to exist authentication
💡 Result:
Short answer: No — emailAddresses are not universally guaranteed to exist for every authenticated Clerk user. Clerk users can authenticate via phone, OAuth/SAML, Web3, passkeys, etc., so a user may have no email addresses on file. Check user.emailAddresses (or user.primaryEmailAddress) for null/empty and handle that case in your code. [1][2][3]
Sources:
- Clerk Backend User types (emailAddresses, primaryEmailAddress described). [1]
- SignIn/PrepareFirstFactor doc (lists auth strategies that don't require email). [2]
- Guides noting how email addresses are added/verified when using email flows. [3]
Add email validation guard before Prisma lookup (affects both PATCH and DELETE).
With Prisma 6.7.0 (used here), passing undefined to a where clause silently removes the filter, causing where: { email: undefined } to match any user. Since emailAddresses can be undefined when users authenticate via phone, OAuth, or SAML, this risks returning an arbitrary tenant ID. Add an explicit guard before the Prisma query and apply the same fix in the DELETE endpoint.
🐛 Suggested fix
const email = user.emailAddresses[0]?.emailAddress;
const role = (user.publicMetadata.role as string) || "";
+if (!email) {
+ return NextResponse.json({ error: "User email not found" }, { status: 401 });
+}
+
const localUser = await prisma.users.findFirst({
- where: { email: email },
+ where: { email },
select: { tenantId: true },
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const email = user.emailAddresses[0]?.emailAddress; | |
| const role = (user.publicMetadata.role as string) || ""; | |
| if (role !== "TENANT_ADMIN") { | |
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | |
| } | |
| const localUser = await prisma.users.findFirst({ | |
| where: { email: email }, | |
| select: { tenantId: true }, | |
| }); | |
| const tenantId = localUser?.tenantId; | |
| if (!tenantId) { | |
| const email = user.emailAddresses[0]?.emailAddress; | |
| const role = (user.publicMetadata.role as string) || ""; | |
| if (role !== "TENANT_ADMIN") { | |
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | |
| } | |
| if (!email) { | |
| return NextResponse.json({ error: "User email not found" }, { status: 401 }); | |
| } | |
| const localUser = await prisma.users.findFirst({ | |
| where: { email }, | |
| select: { tenantId: true }, | |
| }); | |
| const tenantId = localUser?.tenantId; | |
| if (!tenantId) { |
🤖 Prompt for AI Agents
In `@nextjs_space/app/api/tenant-admin/webhooks/`[id]/route.ts around lines 22 -
35, The Prisma lookup uses email which may be undefined and will remove the
filter in Prisma 6.7.0; add an explicit guard that verifies email (from
user.emailAddresses[0]?.emailAddress) is a non-empty string before calling
prisma.users.findFirst in the PATCH handler (route.ts) and return an error
(e.g., 400/401 JSON) if missing, then apply the identical guard to the DELETE
endpoint; update the logic around the localUser/tenantId fetching so
prisma.users.findFirst only runs when email is truthy to avoid accidental
full-table matches.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
nextjs_space/app/api/user/profile/route.ts (1)
26-37: Partial address updates will overwrite existing fields with empty strings.When any address field is provided, the entire address object is rebuilt with empty strings for missing fields. This causes unintended data loss—e.g., if a user only updates
city, their existingaddressLine1,postalCode, etc. are wiped.Consider either:
- Fetching the existing address first and merging, or
- Only including fields that were explicitly provided in the request
🛠️ Proposed fix: Only include provided fields
- // Build address object if any address fields provided - const address = - addressLine1 || city || state || postalCode || country - ? { - addressLine1: addressLine1 || "", - addressLine2: addressLine2 || "", - city: city || "", - state: state || "", - postalCode: postalCode || "", - country: country || "", - } - : undefined; + // Build address object only with provided fields + const addressFields = { + ...(addressLine1 !== undefined && { addressLine1 }), + ...(addressLine2 !== undefined && { addressLine2 }), + ...(city !== undefined && { city }), + ...(state !== undefined && { state }), + ...(postalCode !== undefined && { postalCode }), + ...(country !== undefined && { country }), + }; + const address = Object.keys(addressFields).length > 0 ? addressFields : undefined;nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts (1)
2-39: DELETE method is missing email validation before Prisma query — critical issue.The PATCH method correctly guards against missing email (lines 25–27) before using it for the tenant lookup. The DELETE method extracts the email at line 120 but lacks this guard before calling
prisma.users.findFirst({ where: { email: email } })at line 128. Ifuser.emailAddresses[0]?.emailAddressis undefined, the Prisma query will execute with an undefined email value.Additionally, line 162 uses a non-null assertion (
email!) in the audit log, but the email may legitimately be undefined at that point.Add an email null check to DELETE matching the PATCH pattern:
Suggested fix for DELETE
const email = user.emailAddresses[0]?.emailAddress; const role = (user.publicMetadata.role as string) || ""; + if (!email) { + return NextResponse.json({ error: "Email not found" }, { status: 401 }); + } if (role !== "TENANT_ADMIN") {Clerk API usage (
currentUser()andpublicMetadata.roleaccess) is correct for route handlers.
🤖 Fix all issues with AI agents
In `@nextjs_space/app/api/store/`[slug]/cart/add/route.ts:
- Around line 18-29: The lookup uses user.emailAddresses[0] which may not be the
user's primary email; update the code that sets email (the variable used for
prisma.users.findUnique) to use Clerk's primaryEmailAddress first
(user.primaryEmailAddress?.emailAddress) and fall back to the existing
emailAddresses array if needed (e.g., find the primary entry or use index 0),
then pass that email to prisma.users.findUnique so multi-email users resolve
correctly.
In `@nextjs_space/app/api/store/`[slug]/orders/route.ts:
- Around line 17-24: The code currently uses
user.emailAddresses[0]?.emailAddress which can be non-primary; instead, read
user.primaryEmailAddressId and locate the matching entry in user.emailAddresses
(e.g., find(e => e.id === user.primaryEmailAddressId)) to extract the
authoritative emailAddress, fall back to a verified/primary strategy only if
needed, and then pass that email into prisma.users.findFirst; update the logic
around the user, emailAddresses, primaryEmailAddressId and the
prisma.users.findFirst call in route.ts so you return 401 only when no primary
email is found.
In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts:
- Around line 140-161: The PATCH/DELETE handlers lack a guard for a missing
localUser from prisma.users.findFirst which lets tenant checks using
localUser?.tenantId behave incorrectly; update the PATCH and DELETE flows (same
pattern used in GET) to immediately return a 404 (or 401) when localUser is null
for non-SUPER_ADMINs after calling currentUser() and resolving email/role,
before any tenantId comparisons, so that TENANT_ADMINs without a mapped
localUser cannot proceed; ensure you reference and guard the localUser variable
in the handlers that perform tenant scoping (the sections around the existing
localUser = await prisma.users.findFirst(...) and the subsequent
localUser?.tenantId checks).
In `@nextjs_space/scripts/seed-clerk-orgs.ts`:
- Around line 85-111: The membership creation catch block is checking the wrong
Clerk error codes; update the condition in the catch for
clerkClient.organizations.createOrganizationMembership so it treats the Clerk
error code 'already_a_member_in_organization' as the benign case: inspect the
caught error variable (e) and if e.errors && e.errors[0]?.code ===
'already_a_member_in_organization' log the "User already a member" message,
otherwise rethrow or log the failure as before; keep the existing
clerkUserList/clerkUserId resolution and logging intact.
♻️ Duplicate comments (3)
nextjs_space/app/api/user/profile/route.ts (1)
45-54: LGTM! Previous critical issue resolved.The update now correctly uses
where: { id: user.id }instead of the previously flaggeduser.email!. This aligns with the codebase pattern and avoids potential runtime errors from undefined email values.nextjs_space/app/tenant-admin/seo/page.tsx (1)
18-33: Prefer primary email to avoid mismatched tenant lookup.
emailAddresses[0]can select a secondary/unverified email; if local users are keyed by the primary email, multi‑address users may fail lookup and get redirected. ConsiderprimaryEmailAddressfirst (optionally with a fallback).🔧 Suggested change
- const email = user.emailAddresses[0]?.emailAddress; + const email = + user.primaryEmailAddress?.emailAddress ?? user.emailAddresses[0]?.emailAddress;nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts (1)
120-130: Add the missing email guard in DELETE to avoid undefined Prisma filters.
Ifwhere: { email: undefined }can drop the filter and return an arbitrary user, leading to incorrect tenant association and audit attribution. Mirror the PATCH guard here.🐛 Suggested fix
const email = user.emailAddresses[0]?.emailAddress; const role = (user.publicMetadata.role as string) || ""; +if (!email) { + return NextResponse.json({ error: "Email not found" }, { status: 401 }); +} + if (role !== "TENANT_ADMIN") { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const localUser = await prisma.users.findFirst({ - where: { email: email }, + where: { email }, select: { tenantId: true }, });Please confirm Prisma’s undefined-where behavior for your version.
Prisma Client undefined value in where clause behavior (current version)Also applies to: 161-163
🧹 Nitpick comments (4)
nextjs_space/scripts/seed-clerk-orgs.ts (1)
11-14: Consider validating required environment variables.If
CLERK_SECRET_KEYis missing or undefined, the script may fail with an unclear error downstream. Adding an early guard improves debuggability for developers running this seed script.💡 Suggested validation
+if (!process.env.CLERK_SECRET_KEY) { + console.error('❌ CLERK_SECRET_KEY is not set. Please check your .env.local file.'); + process.exit(1); +} + const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, });nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts (3)
26-31: Prefer deterministic user lookup and confirm tenant cardinality.If
users.emailis unique,findUniqueavoids accidental duplicates and is clearer. Also confirm whetherusers.tenantsis 1‑to‑1; if it’s a list,localUser.tenants.settingswill fail unless you select a tenant explicitly.♻️ Proposed change (if email is unique)
- const localUser = await prisma.users.findFirst({ - where: { email: email }, + const localUser = await prisma.users.findUnique({ + where: { email }, include: { tenants: true }, });Also applies to: 44-44
79-80: Return SMTP verify errors as 400 (align with super‑admin flow).Connection verify failures are usually configuration issues, so returning a 400 with error details (similar to
nextjs_space/app/api/super-admin/test-smtp/route.tslines 6‑156) gives better UX than a generic 500.♻️ Proposed change
- console.log(`[TenantSMTP] Verifying connection for ${localUser.tenants.id}...`); - await transporter.verify(); + console.log(`[TenantSMTP] Verifying connection for ${localUser.tenants.id}...`); + try { + await transporter.verify(); + } catch (err: any) { + console.error("[TenantSMTP] Connection verification failed:", err); + return NextResponse.json( + { + error: `SMTP connection failed: ${err.message}`, + details: { code: err.code, command: err.command }, + }, + { status: 400 }, + ); + }
83-84: SanitizefromName/fromEmailbefore building headers.These values are tenant‑controlled; stripping CR/LF (and quotes in the name) prevents header injection and reduces send failures if invalid characters slip in. If validation already happens on save, consider reusing that sanitization here.
♻️ Proposed change
- const fromAddress = smtp.fromEmail - ? `"${smtp.fromName || localUser.tenants.businessName}" <${smtp.fromEmail}>` - : `"${localUser.tenants.businessName}" <${smtp.user}>`; + const safeFromName = (smtp.fromName || localUser.tenants.businessName || "") + .replace(/[\r\n"]/g, "") + .trim(); + const safeFromEmail = (smtp.fromEmail || "") + .replace(/[\r\n]/g, "") + .trim(); + + const fromAddress = safeFromEmail + ? `"${safeFromName}" <${safeFromEmail}>` + : `"${safeFromName}" <${smtp.user}>`;
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
nextjs_space/app/api/store/[slug]/cart/add/route.tsnextjs_space/app/api/store/[slug]/orders/route.tsnextjs_space/app/api/store/[slug]/orders/submit/route.tsnextjs_space/app/api/tenant-admin/customers/[id]/route.tsnextjs_space/app/api/tenant-admin/settings/test-smtp/route.tsnextjs_space/app/api/tenant-admin/webhooks/[id]/route.tsnextjs_space/app/api/user/profile/route.tsnextjs_space/app/store/[slug]/the-wire/[postSlug]/page.tsxnextjs_space/app/tenant-admin/seo/page.tsxnextjs_space/scripts/seed-clerk-orgs.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- nextjs_space/app/store/[slug]/the-wire/[postSlug]/page.tsx
- nextjs_space/app/api/store/[slug]/orders/submit/route.ts
🧰 Additional context used
🧬 Code graph analysis (7)
nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/api/store/[slug]/cart/add/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/api/store/[slug]/orders/route.ts (3)
nextjs_space/app/api/orders/customer/route.ts (1)
GET(5-49)nextjs_space/app/api/orders/route.ts (1)
GET(180-223)nextjs_space/app/api/store/[slug]/cart/route.ts (1)
GET(7-47)
nextjs_space/app/tenant-admin/seo/page.tsx (1)
nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts (2)
nextjs_space/app/api/super-admin/test-smtp/route.ts (1)
POST(7-157)nextjs_space/app/api/tenant-admin/settings/route.ts (1)
POST(7-144)
nextjs_space/app/api/user/profile/route.ts (2)
nextjs_space/lib/auth-helper.ts (1)
getCurrentUser(3-23)nextjs_space/lib/db.ts (1)
prisma(34-43)
🔇 Additional comments (12)
nextjs_space/app/api/user/profile/route.ts (1)
7-11: LGTM! Auth migration correctly implemented.The authentication check using
getCurrentUser()follows the established Clerk-based pattern across the codebase.nextjs_space/app/api/store/[slug]/cart/add/route.ts (1)
70-72: Good: dbUser.id is now used for cart ops.nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (2)
16-40: Clerk auth + tenant scoping in GET looks solid.
Good guardrails around Line 22–27 (email) and Line 29–31 (role), and the tenantId check in Line 88–90 is correctly scoped to TENANT_ADMIN.Also applies to: 88-90
216-217: Audit log attribution update is correct.
Using Clerk’suser.idandAlso applies to: 320-321
nextjs_space/scripts/seed-clerk-orgs.ts (3)
37-47: Good fix: Admin Clerk ID is now resolved correctly.The code properly resolves the Clerk user ID via email lookup before passing it to
createOrganization. This addresses the previous review feedback about using Prisma IDs where Clerk IDs are expected.
130-137: LGTM!The script properly handles cleanup with
prisma.$disconnect()in thefinallyblock, ensuring the database connection is released regardless of success or failure.
63-80: Error handling for duplicate organization slug is correct.The error code
form_identifier_existscorrectly identifies slug conflicts in Clerk's API, and the fallback approach of retrieving the existing organization via admin membership is appropriate.nextjs_space/app/tenant-admin/seo/page.tsx (1)
1-16: Auth guard looks solid.Clear authentication/role gating with early redirects.
nextjs_space/app/api/store/[slug]/orders/route.ts (3)
2-2: Clerk import looks good.
40-41: Good switch todbUser.idfor scoping orders.
26-28: The related order endpoints in this namespace all consistently return 404 for missing db users—no alignment issue.All sibling endpoints within
/api/store/[slug]/orders/*(including[orderId]/route.tsandsubmit/route.ts) return 404 when the db user is not found, matching the behavior in the target file. The endpoint is consistent with its related family and does not represent a breaking change.Likely an incorrect or invalid review comment.
nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts (1)
2-24: Auth/role gate is clean and early.The early unauthorized exits keep the path clear; just verify that admin accounts in Clerk always have
publicMetadata.roleand a primary email populated so valid users aren’t rejected.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| const email = user.emailAddresses[0]?.emailAddress; | ||
| if (!email) { | ||
| return NextResponse.json({ error: "Email not found" }, { status: 401 }); | ||
| } | ||
|
|
||
| const dbUser = await prisma.users.findUnique({ | ||
| where: { email }, | ||
| }); | ||
|
|
||
| if (!dbUser) { | ||
| return NextResponse.json({ error: "User not found in database" }, { status: 404 }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n nextjs_space/app/api/store/[slug]/cart/add/route.ts | head -50Repository: AutomatosAI/budstack-saas
Length of output: 1797
🌐 Web query:
Clerk currentUser User object primaryEmailAddress documentation
💡 Result:
Clerk's User object (returned by currentUser/useUser/etc.) includes these primary email fields:
- primaryEmailAddress — an EmailAddress object or null (information about the user's primary email). [1][2]
- primaryEmailAddressId — string or null (the EmailAddress resource ID set as primary). [1][2]
Note: there are known typing/shape differences between user objects returned by some helpers (e.g., useUser) and currentUser/currentUser server helpers; see the Clerk GitHub issue discussing missing fields like primaryEmailAddress in some contexts. [3]
Sources:
[1] Clerk — User object reference (primaryEmailAddress, primaryEmailAddressId).
[2] Clerk — Users / managing users guide (description of primary email).
[3] Clerk GitHub issue: useUser vs currentUser user object type differences.
Use primary email to avoid failed lookups for multi-email users.
user.emailAddresses[0] is not guaranteed to be the primary email. For users with multiple registered addresses, this can cause a 404 even when they exist in your database. Clerk's primaryEmailAddress field is available on the currentUser() object; use it with a fallback to ensure correct lookups.
Suggested fix
- const email = user.emailAddresses[0]?.emailAddress;
+ const email =
+ user.primaryEmailAddress?.emailAddress ??
+ user.emailAddresses[0]?.emailAddress;
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const email = user.emailAddresses[0]?.emailAddress; | |
| if (!email) { | |
| return NextResponse.json({ error: "Email not found" }, { status: 401 }); | |
| } | |
| const dbUser = await prisma.users.findUnique({ | |
| where: { email }, | |
| }); | |
| if (!dbUser) { | |
| return NextResponse.json({ error: "User not found in database" }, { status: 404 }); | |
| } | |
| const email = | |
| user.primaryEmailAddress?.emailAddress ?? | |
| user.emailAddresses[0]?.emailAddress; | |
| if (!email) { | |
| return NextResponse.json({ error: "Email not found" }, { status: 401 }); | |
| } | |
| const dbUser = await prisma.users.findUnique({ | |
| where: { email }, | |
| }); | |
| if (!dbUser) { | |
| return NextResponse.json({ error: "User not found in database" }, { status: 404 }); | |
| } |
🤖 Prompt for AI Agents
In `@nextjs_space/app/api/store/`[slug]/cart/add/route.ts around lines 18 - 29,
The lookup uses user.emailAddresses[0] which may not be the user's primary
email; update the code that sets email (the variable used for
prisma.users.findUnique) to use Clerk's primaryEmailAddress first
(user.primaryEmailAddress?.emailAddress) and fall back to the existing
emailAddresses array if needed (e.g., find the primary entry or use index 0),
then pass that email to prisma.users.findUnique so multi-email users resolve
correctly.
| const email = user.emailAddresses[0]?.emailAddress; | ||
| if (!email) { | ||
| return NextResponse.json({ error: "Email not found" }, { status: 401 }); | ||
| } | ||
|
|
||
| const dbUser = await prisma.users.findFirst({ | ||
| where: { email }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "route.ts" | grep -E "store.*orders" | head -20Repository: AutomatosAI/budstack-saas
Length of output: 241
🏁 Script executed:
cat -n nextjs_space/app/api/store/\[slug\]/orders/route.tsRepository: AutomatosAI/budstack-saas
Length of output: 1973
🏁 Script executed:
# Search for Clerk imports and user object patterns to understand the type
rg -A 5 -B 5 "user\." nextjs_space/app/api/store/\[slug\]/orders/route.tsRepository: AutomatosAI/budstack-saas
Length of output: 379
🏁 Script executed:
# Check if Clerk types are used in the project
rg "from.*@clerk" --max-count=10Repository: AutomatosAI/budstack-saas
Length of output: 8736
🏁 Script executed:
# Check how emailAddresses and primaryEmailAddress are used elsewhere
rg "emailAddresses\[0\]|primaryEmailAddress|primaryEmailAddressId" -B 2 -A 2Repository: AutomatosAI/budstack-saas
Length of output: 25770
🏁 Script executed:
# Look for Clerk user type definitions or how user object is handled elsewhere
rg "currentUser\(\)" -A 10 | head -100Repository: AutomatosAI/budstack-saas
Length of output: 6292
🏁 Script executed:
# Search for any Clerk documentation or type imports in the codebase
rg "User|EmailAddress" nextjs_space --type ts --type tsx | grep -i clerk | head -20Repository: AutomatosAI/budstack-saas
Length of output: 96
🌐 Web query:
Clerk currentUser() server-side primaryEmailAddress API documentation
💡 Result:
Short answer: Clerk's server-side helper currentUser() (e.g., @clerk/nextjs/server currentUser()) returns a Backend API User object that does NOT always include the frontend-only fields like primaryEmailAddress. If you need the user's primaryEmailAddress server-side, call the Clerk Admin/Backend user API (clerk.users.getUser or clerkClient.users.getUser) with the user's id to get the full User object (or fetch /v1/users/:id) which includes primaryEmailAddress. [1][2][3]
References:
- currentUser() / Backend API docs (explanation of currentUser usage). [1]
- User object / reference (shows User.primaryEmailAddress and related user fields available via the API). [2]
- GitHub issue noting the difference between useUser() (frontend) and currentUser() (server) and that primaryEmailAddress may be missing from currentUser(). [3]
If you want, I can paste a minimal server-side example showing how to get primaryEmailAddress with clerk.users.getUser.
🌐 Web query:
Clerk User object emailAddresses primaryEmailAddress verified property
💡 Result:
Short answer:
- User.emailAddresses is an array of EmailAddress objects (includes both verified and unverified addresses). [1]
- User.primaryEmailAddress (or User.primaryEmailAddressId) is the single email the user has marked as primary; primaryEmailAddressId holds that EmailAddress id. [1]
- Each EmailAddress object has a verified property (boolean) that indicates whether that specific email address has been verified via Clerk's verification flow. You can create/ update an EmailAddress with verified or change it via the EmailAddress API. [2][3]
Sources: [1] Clerk User reference, [2] Clerk EmailAddress reference, [3] EmailAddress API (create/update schema).
🏁 Script executed:
# Check if there are any package.json files to see Clerk version being used
find . -name "package.json" -type f -exec grep -l "clerk" {} \; | head -5Repository: AutomatosAI/budstack-saas
Length of output: 96
🏁 Script executed:
# Look for tsconfig or type definitions that might give us Clerk type info
fd -e "d.ts" | grep -i clerk | head -10Repository: AutomatosAI/budstack-saas
Length of output: 51
🏁 Script executed:
# Check for any comments in the code about email handling
rg -i "primary.*email|verified.*email" -B 2 -A 2 | head -50Repository: AutomatosAI/budstack-saas
Length of output: 4265
🏁 Script executed:
# Check Clerk version in package.json
cat nextjs_space/package.json | grep -A 2 -B 2 "@clerk"Repository: AutomatosAI/budstack-saas
Length of output: 254
🏁 Script executed:
# Look for any examples in the codebase of using clerkClient or fetching full user data server-side
rg "clerkClient\.users|clerk\.users" -B 2 -A 5 | head -80Repository: AutomatosAI/budstack-saas
Length of output: 4996
🏁 Script executed:
# Check if primaryEmailAddressId is used anywhere in the codebase
rg "primaryEmailAddressId" | head -20Repository: AutomatosAI/budstack-saas
Length of output: 51
🏁 Script executed:
# Look at the webhook handler to see how it handles user data from Clerk events
cat -n nextjs_space/app/api/webhooks/clerk/route.ts | head -100Repository: AutomatosAI/budstack-saas
Length of output: 4734
🏁 Script executed:
# Check if primaryEmailAddressId is used anywhere in the codebase with currentUser
rg "currentUser\(\)" -A 20 | rg -B 5 -A 5 "primaryEmailAddressId"Repository: AutomatosAI/budstack-saas
Length of output: 51
🏁 Script executed:
# Let's also verify what properties are available on user object from currentUser
# by looking at how user is accessed in these server-side routes
rg "const user = await currentUser" -A 30 | head -100Repository: AutomatosAI/budstack-saas
Length of output: 7136
Use the primary email address, not the first address in the array.
emailAddresses[0] can be non-primary or unverified, which can mis-map users or return false "not found" for valid accounts. The server-side currentUser() object includes primaryEmailAddressId; use it to find the correct email address.
🔧 Suggested adjustment
- const email = user.emailAddresses[0]?.emailAddress;
+ const email =
+ user.emailAddresses.find(
+ (address) => address.id === user.primaryEmailAddressId,
+ )?.emailAddress ?? user.emailAddresses[0]?.emailAddress;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const email = user.emailAddresses[0]?.emailAddress; | |
| if (!email) { | |
| return NextResponse.json({ error: "Email not found" }, { status: 401 }); | |
| } | |
| const dbUser = await prisma.users.findFirst({ | |
| where: { email }, | |
| }); | |
| const email = | |
| user.emailAddresses.find( | |
| (address) => address.id === user.primaryEmailAddressId, | |
| )?.emailAddress ?? user.emailAddresses[0]?.emailAddress; | |
| if (!email) { | |
| return NextResponse.json({ error: "Email not found" }, { status: 401 }); | |
| } | |
| const dbUser = await prisma.users.findFirst({ | |
| where: { email }, | |
| }); |
🤖 Prompt for AI Agents
In `@nextjs_space/app/api/store/`[slug]/orders/route.ts around lines 17 - 24, The
code currently uses user.emailAddresses[0]?.emailAddress which can be
non-primary; instead, read user.primaryEmailAddressId and locate the matching
entry in user.emailAddresses (e.g., find(e => e.id ===
user.primaryEmailAddressId)) to extract the authoritative emailAddress, fall
back to a verified/primary strategy only if needed, and then pass that email
into prisma.users.findFirst; update the logic around the user, emailAddresses,
primaryEmailAddressId and the prisma.users.findFirst call in route.ts so you
return 401 only when no primary email is found.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)
195-200: Handle empty-string name updates explicitly.The truthy check skips updates when
firstName/lastNameare empty strings, leavingnamestale. If empty strings are valid for “clear,” use!== undefined.✅ Suggested fix
- ...(firstName && lastName && { name: `${firstName} ${lastName}` }), + ...(firstName !== undefined && + lastName !== undefined && { name: `${firstName} ${lastName}`.trim() }),
🤖 Fix all issues with AI agents
In `@nextjs_space/app/api/orders/route.ts`:
- Line 88: The code is currently trusting clientId from the request body when
building the order (client_id: clientId || dbUser.id), which allows
impersonation; change it to derive client_id from the authenticated user (use
dbUser.id) and do not accept clientId unless validated and authorized—if you
must support an override, validate that the authenticated user has an admin role
(e.g., dbUser.role === 'admin') or that clientId equals dbUser.id before using
it; update the order creation site (where client_id is set) to ignore
unvalidated clientId values and enforce the server-derived dbUser.id (or an
explicitly-authorized override).
- Around line 27-31: The user lookup uses prisma.users.findFirst with only email
which lacks tenant scoping; fetch the tenant context using
getTenantFromRequest(req) (or ensure tenantId is obtained first) and include
tenantId in the findFirst where clause (i.e., prisma.users.findFirst({ where: {
email, tenantId } })) so dbUser is resolved within the tenant boundary; update
the code around prisma.users.findFirst and any flow that depends on dbUser to
use the tenant-scoped lookup.
In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts:
- Around line 22-27: The code currently picks the email with
user.emailAddresses[0], which is fragile; instead resolve the primary email by
matching user.primaryEmailAddressId against user.emailAddresses[*].id and use
that emailAddress value, falling back to user.emailAddresses[0]?.emailAddress if
no match; update the GET handler block where const email is set, and make the
identical change in the PATCH and DELETE handlers so all uses of
user.emailAddresses use the resolved primaryEmailAddressId lookup before falling
back, keeping the existing NextResponse.json({ error: "Email not found" }, {
status: 401 }) behavior when no email is found.
🧹 Nitpick comments (2)
nextjs_space/scripts/seed-clerk-orgs.ts (1)
11-14: Consider validating required environment variables.If
CLERK_SECRET_KEYis missing or undefined, the script will fail with an unclear error from the Clerk API. Adding upfront validation improves debuggability.♻️ Suggested validation
+if (!process.env.CLERK_SECRET_KEY) { + console.error('❌ CLERK_SECRET_KEY is not set. Ensure .env.local is configured.'); + process.exit(1); +} + const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, });nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)
157-158: Remove duplicate comment to reduce noise.The same comment appears twice in a row.
🧹 Cleanup
- // Fetch local user to get tenantId for authorization - // Fetch local user to get tenantId for authorization + // Fetch local user to get tenantId for authorization
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
nextjs_space/app/api/orders/route.tsnextjs_space/app/api/tenant-admin/customers/[id]/route.tsnextjs_space/scripts/seed-clerk-orgs.ts
🧰 Additional context used
🧬 Code graph analysis (3)
nextjs_space/scripts/seed-clerk-orgs.ts (2)
nextjs_space/next.config.js (1)
path(1-1)nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/app/api/orders/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(34-43)
🔇 Additional comments (7)
nextjs_space/scripts/seed-clerk-orgs.ts (6)
1-7: LGTM!Environment loading is correctly ordered before the Clerk client import, ensuring
CLERK_SECRET_KEYis available during initialization.
16-28: LGTM!The tenant query correctly includes
TENANT_ADMINusers for organization seeding.
37-47: LGTM!The admin Clerk ID resolution via email lookup is the correct approach, addressing the previous concern about mixing Prisma and Clerk user IDs.
49-81: LGTM!Organization creation correctly uses the resolved Clerk admin ID, and the fallback lookup via membership list gracefully handles existing organizations.
83-120: LGTM!Member resolution and error handling are correctly implemented. The
already_a_member_in_organizationerror code matches Clerk's API response for duplicate memberships.
130-137: LGTM!Script execution pattern is correct with proper error handling and Prisma disconnection in the
finallyblock.nextjs_space/app/api/orders/route.ts (1)
194-198: Use Clerk’s primary email for DB lookup in GET to avoid mismatched users.If
emailAddresses[0]isn’t guaranteed to be the primary email, this can resolve a different user and surface another account’s orders. Align GET with POST’s primary-email resolution.🔧 Proposed fix
- const email = user.emailAddresses[0].emailAddress; + const primaryEmail = + user.emailAddresses.find(e => e.id === user.primaryEmailAddressId)?.emailAddress; + const email = primaryEmail || user.emailAddresses[0]?.emailAddress;
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| const email = user.emailAddresses[0]?.emailAddress; | ||
| const role = (user.publicMetadata.role as string) || ""; | ||
|
|
||
| if (!email) { | ||
| return NextResponse.json({ error: "Email not found" }, { status: 401 }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Clerk currentUser primaryEmailAddressId emailAddresses API documentation recommended access
💡 Result:
Summary — where these fields/methods live and recommended access
-
primaryEmailAddressId: property on the Clerk User object (string | null). Available in both frontend (ClerkJS / client SDK) and backend user objects. Use it to get the ID of the user's primary EmailAddress. [1][3]
-
emailAddresses (list): Clerk exposes the user's email addresses via the EmailAddress resource (e.g., user.emailAddresses()) — returns all email addresses (verified and unverified), including the primary. This endpoint/method is available in SDKs (frontend and backend variants depending on SDK). [6]
-
Changing the primary email: set primaryEmailAddressId (or primaryEmailAddressID in backend updateUser) when updating the user. The email you set must already exist on the user and be verified. Backend updateUser supports notifyPrimaryEmailAddressChanged flag. Use the backend SDK for authoritative changes. [1][2]
Recommended access patterns
- Read-only display of current user primary email: safe to read currentUser.primaryEmailAddressId (and resolve to the EmailAddress object from user.primaryEmailAddress or emailAddresses list) on the frontend. [1][3]
- To list or manage all email addresses, or to make authoritative changes (set primary, require verification, send notifications), perform those operations via the Backend SDK / Backend API (updateUser or the backend User/emailAddresses endpoints). This ensures proper security and server-side validation. [2][6]
Sources
[1] Clerk User object (JS SDK) — primaryEmailAddressId, update() params.
[2] Clerk Backend SDK — updateUser() (primaryEmailAddressID, notifyPrimaryEmailAddressChanged).
[3] Clerk Backend User object reference — primaryEmailAddress accessor.
[6] Clerk SDK / API emailAddresses (list) method.
🌐 Web query:
Clerk @clerk/nextjs server currentUser primaryEmailAddress accessor backend User object
💡 Result:
Short answer
- The server-side currentUser() from
@clerk/nextjs/serverreturns the Backend "User" object but its shape differs from the client-side useUser() result: currentUser() may not include convenience fields like primaryEmailAddress/fullName that the client object exposes. Use the User fields that are present (emailAddresses, primaryEmailAddressId) or fetch the full Backend User via the backend SDK to get the primary-email info. [1][2][3]
How to access the primary email (two options)
- If currentUser() includes primaryEmailAddress:
- Access it directly:
const user = await currentUser();
const email = user.primaryEmailAddress?.emailAddress;
- If currentUser() does NOT include primaryEmailAddress (safe server approach):
- Use clerkClient to fetch the full backend User (or lookup from user.emailAddresses):
import { currentUser, clerkClient } from '@clerk/nextjs/server'
const me = await currentUser()
if (!me) return null
// fetch full backend user by id to ensure all backend fields are present
const fullUser = await clerkClient.users.getUser(me.id)
const primary = fullUser.primaryEmailAddress?.emailAddress
// OR find by id from emailAddresses array:
const primaryFromList = fullUser.emailAddresses.find(e => e.id === fullUser.primaryEmailAddressId)?.emailAddress
Why this is needed
- Clerk docs state currentUser() returns a Backend User object but caution about differences vs client user objects; a reported GitHub issue documents the inconsistency (primaryEmailAddress exists in client useUser but not always on server currentUser()). Use clerkClient.users.getUser() when you need the reliable backend fields. [1][2][3][4]
References
- Clerk currentUser() docs. [1]
- GitHub issue about different shapes (useUser vs currentUser). [2]
- Clerk User object reference (primaryEmailAddress, emailAddresses, primaryEmailAddressId). [3]
- Example using clerkClient to map users and primary email. [4]
🏁 Script executed:
# Verify the file and lines mentioned in the review
fd "route.ts" | grep "tenant-admin/customers"Repository: AutomatosAI/budstack-saas
Length of output: 179
🏁 Script executed:
cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | head -50Repository: AutomatosAI/budstack-saas
Length of output: 1858
🏁 Script executed:
cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '200,250p'Repository: AutomatosAI/budstack-saas
Length of output: 1655
🏁 Script executed:
cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '155,165p'Repository: AutomatosAI/budstack-saas
Length of output: 493
🏁 Script executed:
cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '130,155p'Repository: AutomatosAI/budstack-saas
Length of output: 1009
🏁 Script executed:
cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '250,290p'Repository: AutomatosAI/budstack-saas
Length of output: 1588
Use Clerk's primaryEmailAddressId to resolve the primary email address.
Using index 0 is fragile—if the email addresses array reorders, a non-primary email may be selected and break local user mapping. Resolve by ID using primaryEmailAddressId and fallback to index 0. Apply the same change in PATCH and DELETE.
🔧 Suggested adjustment (GET block; mirror in PATCH/DELETE)
- const email = user.emailAddresses[0]?.emailAddress;
+ const primaryEmail =
+ user.emailAddresses.find(
+ (addr) => addr.id === user.primaryEmailAddressId,
+ )?.emailAddress;
+ const email = primaryEmail ?? user.emailAddresses[0]?.emailAddress;🤖 Prompt for AI Agents
In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts around lines 22 -
27, The code currently picks the email with user.emailAddresses[0], which is
fragile; instead resolve the primary email by matching
user.primaryEmailAddressId against user.emailAddresses[*].id and use that
emailAddress value, falling back to user.emailAddresses[0]?.emailAddress if no
match; update the GET handler block where const email is set, and make the
identical change in the PATCH and DELETE handlers so all uses of
user.emailAddresses use the resolved primaryEmailAddressId lookup before falling
back, keeping the existing NextResponse.json({ error: "Email not found" }, {
status: 401 }) behavior when no email is found.
Summary by CodeRabbit
New Features
Bug Fixes & Improvements
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.