Fix Dr. Green address mapping, tighten admin APIs, add tenant scoping and assorted fixes#8
Conversation
📝 WalkthroughWalkthroughThis pull request updates API endpoints with authentication and validation logic, centralizes the Dr. Green API client integration, enhances UI components with accessibility features and CSS sanitization, implements Redis-backed rate limiting, adds encryption migration support, refines tenant-scoping mechanisms, and corrects display name typos across UI components. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93c49e8153
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Find user by email | ||
| const user = await prisma.users.findUnique({ | ||
| const user = await prisma.users.findFirst({ | ||
| where: { email }, | ||
| }); |
There was a problem hiding this comment.
Scope password reset lookup to tenant
This handler now uses findFirst by email without setting tenant context, so in the new schema where users.email is no longer globally unique, a reset request can match an arbitrary tenant’s user record. If the same email exists in multiple tenants, the reset token is stored on (and later resets) the wrong account. Please include tenant scoping (e.g., tenantId from the request context) or otherwise disambiguate before generating the token.
Useful? React with 👍 / 👎.
| const decryptedApiKey = decrypt(tenant.drGreenApiKey); | ||
| const decryptedSecret = decrypt(tenant.drGreenSecretKey); | ||
|
|
||
| if (!decryptedSecret) { | ||
| throw new Error("Failed to decrypt Dr Green Secret Key. Please update your settings."); | ||
| if (!decryptedApiKey || !decryptedSecret) { | ||
| throw new Error("Failed to decrypt Dr Green credentials. Please update your settings."); |
There was a problem hiding this comment.
Avoid breaking existing plaintext Dr. Green keys
The API key is now always decrypted and treated as invalid if decryption fails. Previously drGreenApiKey was stored in plaintext, so existing tenants will now throw when decrypt sees a non-iv:tag:cipher value, causing all Dr. Green calls to fail until the key is re-saved. Consider a migration/fallback (e.g., allow unencrypted during a window) or conditional decryption to avoid breaking existing tenants.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (17)
nextjs_space/app/tenant-admin/branding/page.tsx (1)
8-15: MissingauthOptionsand guard/query mismatch.Two issues with this change:
getServerSession()is called without passingauthOptions. In Next.js App Router, the session callback that addsid,role, andtenantId(as shown inlib/auth.ts) only runs when auth options are provided. Without it,session.user.idwill likely beundefined.The guard on line 10 validates
session?.user?.emailexists, but line 15 usessession.user.id. These should be consistent—if you're querying byid, validate thatidexists.Proposed fix
-import { getServerSession } from 'next-auth'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth';export default async function BrandingPage() { - const session = await getServerSession(); + const session = await getServerSession(authOptions); - if (!session?.user?.email) { + if (!session?.user?.id) { redirect('/auth/login'); } const user = await prisma.users.findUnique({ where: { id: session.user.id },nextjs_space/lib/webhook.ts (1)
124-127: setTimeout-based retries are unreliable in serverless environments.In Next.js API routes and serverless functions,
setTimeoutcallbacks may never execute because the runtime can terminate after the response is sent. Failed webhook deliveries may silently drop without retry.Consider using a persistent job queue (e.g., BullMQ with Redis, which this PR already integrates for rate limiting) or database-backed retry scheduling for reliable delivery.
nextjs_space/app/super-admin/platform-settings/page.tsx (1)
9-21: MissingauthOptionswill causesession.user.idto be undefined.
getServerSession()is called withoutauthOptions, but the session callback that populatessession.user.id(inlib/auth.ts) only runs whenauthOptionsis provided. This will causesession.user.idto beundefined, making thefindUniquequery fail to find the user.Additionally, the guard checks
session?.user?.emailbut the query usessession.user.id— consider checking foridinstead if that's the intended lookup key.Proposed fix
+import { authOptions } from '@/lib/auth'; + export default async function PlatformSettingsPage() { - const session = await getServerSession(); + const session = await getServerSession(authOptions); - if (!session?.user?.email) { + if (!session?.user?.id) { redirect('/auth/login'); }nextjs_space/app/super-admin/settings/page.tsx (1)
9-21: MissingauthOptionswill causesession.user.idto be undefined.Same issue as in
platform-settings/page.tsx:getServerSession()is called withoutauthOptions, so the session callback that setssession.user.idwon't execute. The query on line 16 will fail to match any user.Proposed fix
+import { authOptions } from '@/lib/auth'; + export default async function PlatformSettingsConfigPage() { - const session = await getServerSession(); + const session = await getServerSession(authOptions); - if (!session?.user?.email) { + if (!session?.user?.id) { redirect('/auth/login'); }nextjs_space/app/super-admin/templates/page.tsx (1)
13-25: MissingauthOptionswill causesession.user.idto be undefined.Same issue as in the other super-admin pages:
getServerSession()is called withoutauthOptions, sosession.user.idwon't be populated by the session callback. The user lookup on line 20 will fail.Proposed fix
+import { authOptions } from '@/lib/auth'; + export default async function TemplatesManagementPage() { - const session = await getServerSession(); + const session = await getServerSession(authOptions); - if (!session?.user?.email) { + if (!session?.user?.id) { redirect('/auth/login'); }nextjs_space/app/api/shop/register/route.ts (3)
11-18: Addsession.user.idto the authorization check.The code checks for
session?.user?.emailbut later usessession.user.idat line 79 for the database update. Ifidis missing from the session token, the database query will fail with an unclear error. Based on thelib/auth.tssnippet,idis added to the session, but it's safer to validate its presence upfront.Suggested fix
- if (!session?.user?.email) { + if (!session?.user?.email || !session.user.id) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); }
40-53: Return early when credentials are missing.The code logs an error when credentials are missing (lines 48-53) but continues execution, deferring failure to
createClient. This results in unclear error messages for users and potential leakage of internal error details. The comment indicates uncertainty about the intended behavior.Suggested fix
if (!config.apiKey || !config.secretKey) { console.error('Missing Dr. Green API credentials for registration'); - // Continue anyway? The createClient might fail or we should block. - // createClient throws 'MISSING_CREDENTIALS` if keys are missing. - // We'll let it proceed and fail inside if needed, or handle it here. + return NextResponse.json( + { error: 'Service configuration error. Please contact support.' }, + { status: 503 } + ); }
90-96: Avoid exposing internal error messages to clients.Returning
error.messagedirectly can leak sensitive implementation details (database errors, API responses, etc.). Return a generic message for 500 errors while logging the full error server-side.Suggested fix
} catch (error: any) { console.error('Patient registration error:', error); return NextResponse.json( - { error: error.message || 'Failed to register patient' }, + { error: 'Failed to register patient. Please try again or contact support.' }, { status: 500 } ); }nextjs_space/app/api/tenant-admin/products/bulk/route.ts (1)
36-49: Usesession.user.tenantIddirectly instead of querying the database.The session callback in
nextjs_space/lib/auth.ts(line 65) already populatestenantIdonsession.userfrom the JWT token. This database query is redundant and adds unnecessary latency on every request. The webhooks routes already use this optimized pattern successfully.♻️ Proposed refactor
- // Get user's tenant ID - const user = await prisma.users.findUnique({ - where: { id: session.user.id }, - select: { tenantId: true }, - }); - - if (!user?.tenantId) { + const tenantId = (session.user as any).tenantId; + if (!tenantId) { return NextResponse.json( { error: 'No tenant associated with user' }, { status: 403 } ); } - - const tenantId = user.tenantId;Note: This same pattern appears in other tenant-admin routes (orders/bulk, seo/pages, seo/products) and should be refactored consistently.
nextjs_space/app/api/tenant-admin/settings/route.ts (1)
56-63: Inconsistent error handling: SMTP password encryption failure is silently swallowed.Unlike
drGreenSecretKeyanddrGreenApiKey(lines 72-89) which throw on encryption failure, SMTP password encryption failure is caught and silently continued. This could result in the SMTP password not being stored at all, leading to confusing behavior.Suggested fix for consistency
// Handle Password Encryption if (smtpPassword && smtpPassword.trim() !== '') { try { smtpSettings.password = encrypt(smtpPassword); } catch (e) { console.error('SMTP Password Encryption failed:', e); + throw e; } }nextjs_space/lib/encryption.ts (1)
88-91: Silent failure on decryption error could mask issues.Returning an empty string on decryption failure allows the application to continue with invalid credentials. This could lead to confusing errors downstream or security issues if empty strings are treated as valid.
Proposed fix to throw on decryption failure
} catch (error) { console.error('Decryption failed:', error); - return ''; + throw new Error('Decryption failed - encrypted value may be corrupted or key mismatch'); }If returning empty string is intentional for graceful degradation, document this behavior and ensure callers explicitly handle empty results.
nextjs_space/app/api/user/profile/route.ts (1)
10-15: Auth check validatesid— ensureidis present.The guard at line 10 checks
session?.user?.email, but the update at line 34 relies onsession.user.id. Based onlib/auth.ts,idis injected via(session.user as any).id = token.id, so it should be present. However, the explicit check doesn't guaranteeidexists.Suggested fix: Check for id explicitly
- if (!session?.user?.email) { + if (!session?.user?.id) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); }Also applies to: 33-34
nextjs_space/app/api/auth/reset-password/route.ts (1)
46-48: Duplicate comment.The comment
// Send password reset emailappears twice.Remove duplicate
// Send password reset email - // Send password reset email const html = await emailTemplates.passwordReset(user.name || 'User', resetLink, 'BudStack');nextjs_space/app/api/onboarding/route.ts (1)
150-152: Duplicate comment.Remove duplicate
// Send tenant welcome email (don't wait for it) - // Send tenant welcome email (don't wait for it) const html = await emailTemplates.tenantWelcome(businessName, businessName, subdomain);nextjs_space/app/api/reset-password/route.ts (2)
6-6: Use the shared Prisma client fromlib/dbinstead of creating a new instance.Creating a new
PrismaClientper request exhausts connection pool limits and bypasses any middleware or configuration (e.g., tenant scoping) applied to the shared client.Suggested fix
-import { PrismaClient } from '@prisma/client'; +import { prisma } from '@/lib/db'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; -const prisma = new PrismaClient();Also remove the
finallyblock withprisma.$disconnect()at lines 82-84.
24-59: Security/correctness concern: Password reset endpoint auto-creates users with hardcoded data.This flow creates a new user with the hardcoded name
'Gerard Kavanagh'under the'healingbuds'tenant when the email is not found. This appears to be test/debug code that should not be in production:
- Password reset should not create accounts — it should only reset existing passwords
- The hardcoded name is clearly test data
- Auto-provisioning users on a reset endpoint is a security risk (account enumeration, unintended account creation)
Suggested fix: Return not-found or generic message
if (!user) { - // Get tenant for healingbuds - const tenant = await prisma.tenants.findUnique({ - where: { subdomain: 'healingbuds' }, - }); - - if (!tenant) { - return NextResponse.json( - { error: 'Tenant not found' }, - { status: 404 } - ); - } - - // Create user - const hashedPassword = await bcrypt.hash(password, 10); - const newUser = await prisma.users.create({ - data: { - id: crypto.randomUUID(), - email: email.toLowerCase(), - password: hashedPassword, - name: 'Gerard Kavanagh', - role: 'PATIENT', - tenantId: tenant.id, - updatedAt: new Date(), - }, - }); - - return NextResponse.json({ - success: true, - message: 'User created', - user: { - email: newUser.email, - name: newUser.name, - }, - }); + // Return generic message to prevent email enumeration + return NextResponse.json({ + success: true, + message: 'If an account exists, the password has been reset.', + }); }nextjs_space/lib/doctor-green-api.ts (1)
85-89: Dead code: Warning will never execute.Line 89 checks
if (!apiKey)but this condition is already covered by the throw on line 86. IfapiKeyis missing, the function throws before reaching line 89.🧹 Suggested fix
if (!apiKey || !secretKey) { throw new Error('MISSING_CREDENTIALS'); } - if (!apiKey) console.warn('Warning: No Dr Green API Key provided'); - return callDrGreenAPI(endpoint, {
🤖 Fix all issues with AI agents
In @nextjs_space/app/api/shop/register/route.ts:
- Around line 43-45: The catch block that currently logs the entire error object
(in route.ts around the getTenantDrGreenConfig call) may expose sensitive
tenant/credential details; change the log to only include a non-sensitive
message and the error message/text (e.g., use error?.message || String(error))
instead of the full object, or redact sensitive fields before logging; update
the console.warn in that catch to emit a safe, minimal string and avoid
serializing the entire error.
In @nextjs_space/lib/auth.ts:
- Around line 20-27: The user lookup in getCurrentTenant() uses { email,
tenantId: tenant.id } which prevents SUPER_ADMIN users (tenantId: null) from
authenticating on tenant subdomains; update the prisma.users.findFirst where
clause to, when tenant?.id is present, query for email plus an OR on tenantId
equal to tenant.id OR tenantId null (e.g., when tenant?.id then use { email:
credentials.email, OR: [{ tenantId: tenant.id }, { tenantId: null }] },
otherwise keep { email: credentials.email }) so SUPER_ADMIN accounts can
authenticate across subdomains while preserving the tenant-scoped lookup.
In @nextjs_space/lib/db.ts:
- Around line 135-144: The current tenant-scoping branch only guards actions in
tenantScopedReadActions and tenantScopedWriteManyActions, so single-record write
actions like "update" and "delete" bypass scoping; update the condition around
params.action (or expand tenantScopedWriteManyActions) to also include the
single-record write actions ("update" and "delete") so that when params.action
is "update" or "delete" you applyTenantScope to params.args.where (using
applyTenantScope(params.args.where, tenantId, allowNull) or setting
params.args.where when missing) just like the existing write-many handling;
reference tenantScopedReadActions, tenantScopedWriteManyActions, params.action,
applyTenantScope, tenantId, and allowNull when making the change.
In @nextjs_space/lib/rate-limit.ts:
- Around line 59-69: The INCR and PEXPIRE must be executed atomically to avoid
the TTL race; update the Redis pipeline in getRedisClient usage so the MULTI
includes PEXPIRE (e.g., use redis.multi().incr(key).pexpire(key,
windowMs).exec()) instead of calling pexpire after exec, and then read the
increment result from the multi response (results?.[0]?.[1]) as before; it's
safe to call PEXPIRE on every request to ensure TTL is always set.
In @nextjs_space/lib/tenant.ts:
- Around line 160-171: The function leaves AsyncLocalStorage stale because
setTenantContext(null) is only called in the catch path; ensure
setTenantContext(null) is invoked on all fallthroughs: add
setTenantContext(null) before the final return null and also when the subdomain
check (the branch that skips querying for a tenant) is false or when the query
returns a falsy tenant; update references to setTenantContext in this function
(the same scope that calls setTenantContext(tenant.id)) so every exit path
clears the context.
🧹 Nitpick comments (15)
nextjs_space/lib/webhook.ts (1)
67-69: Consider adding proper type for webhook parameter.The
webhook: anycast bypasses type safety. Ifprisma.webhooks.findManyreturns a typed result, you can use that type directly.Suggested improvement
- webhooks.map((webhook: any) => deliverWebhook(webhook.id, payload)) + webhooks.map((webhook) => deliverWebhook(webhook.id, payload))If the Prisma type is inferred correctly, the explicit
anyannotation is unnecessary.nextjs_space/app/api/shop/register/route.ts (1)
20-29: Consider deeper validation of nested fields.The validation only checks for presence of top-level objects (
personal,address,medicalRecord) but doesn't validate required nested fields likefirstName,lastName,createClientwith less clarity.This is a nice-to-have improvement that could provide better error messages to users.
nextjs_space/lib/lovable-converter.ts (2)
393-396: Import pattern is order-dependent and may miss variations.The regex only matches
{ Link, useLocation }in that exact order. If the source has{ useLocation, Link }or includes additional imports like{ Link, useLocation, useNavigate }, this pattern won't match.Suggested more flexible approach
- content = content.replace( - /import \{\s*Link\s*,\s*useLocation\s*\} from ["']react-router-dom["'];?/g, - "import Link from 'next/link';\nimport { usePathname } from 'next/navigation';" - ); + // Handle useLocation -> usePathname replacement + if (content.includes('useLocation') && content.match(/from ["']react-router-dom["']/)) { + content = content.replace(/\buseLocation\b/g, 'usePathname'); + // Ensure usePathname import is added (handled below with Link replacement) + } + + // Replace react-router-dom imports containing Link + content = content.replace( + /import \{[^}]*\bLink\b[^}]*\} from ["']react-router-dom["'];?/g, + "import Link from 'next/link';\nimport { usePathname } from 'next/navigation';" + );Alternatively, consider using an AST-based transformation for more robust handling of import variations.
432-439: Same order-dependency issue as inrefactorComponentFile.Line 433 has the same fragile pattern that only matches
{ Link, useLocation }in exact order. The cleanup at lines 436-439 will catch leftover imports but won't add the necessaryusePathnameimport if the initial pattern didn't match.Consider extracting the import transformation logic into a shared helper function to ensure consistent behavior across
refactorComponentFile,refactorHeaderComponent, andrefactorFooterComponent.nextjs_space/app/api/tenant-admin/select-template/route.ts (1)
12-20: Auth check inconsistency with user lookup.Line 12 checks
session?.user?.emailbut line 20 usessession.user.idfor the lookup. While both should be present perlib/auth.ts, consider checking forsession?.user?.idfor consistency with the actual query:Suggested fix
- if (!session?.user?.email) { + if (!session?.user?.id) {nextjs_space/lib/rate-limit.ts (1)
12-20: Consider connection timeout and error handling for Redis client.With
maxRetriesPerRequest: null, commands will retry indefinitely if Redis is unavailable, potentially causing request hangs. Consider adding connection timeout and max retry limits:Suggested configuration
redisClient = new Redis(REDIS_URL, { - maxRetriesPerRequest: null, + maxRetriesPerRequest: 3, + connectTimeout: 5000, + retryStrategy: (times) => Math.min(times * 100, 3000), lazyConnect: true, });nextjs_space/components/cookie-consent.tsx (2)
141-159: Missing return-focus when modal closes.The modal has good ARIA attributes and backdrop handling, but when the modal closes (via Cancel, Save, Escape, or backdrop click), focus is not returned to the element that opened it. Per WCAG 2.1 guidelines for modal dialogs, focus should return to the trigger element.
Consider storing a ref to the customize button and focusing it when the modal closes:
♻️ Suggested approach
const modalRef = useRef<HTMLDivElement>(null); const firstCheckboxRef = useRef<HTMLInputElement>(null); + const customizeButtonRef = useRef<HTMLButtonElement>(null); + const closeModal = () => { + setShowCustomize(false); + customizeButtonRef.current?.focus(); + };Then use
closeModal()in place ofsetShowCustomize(false)for Cancel, Escape, and backdrop click handlers. Also add the ref to the Customize button in the banner (around line 254).
287-291: Consider escaping the cookie name in regex.The
nameparameter is interpolated directly into a RegExp without escaping special characters. This is safe with the current constant cookie names, but could break if used with names containing regex metacharacters (e.g.,.,+).♻️ Defensive fix
function getCookieValue(name: string): string | undefined { if (typeof document === 'undefined') return undefined; - const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = document.cookie.match(new RegExp('(^| )' + escaped + '=([^;]+)')); return match ? match[2] : undefined; }nextjs_space/app/api/super-admin/tenants/route.ts (2)
33-34: Consider validating pagination parameters.
parseIntwithout bounds checking could cause issues if malformed values are passed.NaN, zero, or negative values would result in incorrect pagination behavior.💡 Suggested validation
- const page = parseInt(searchParams.get('page') || '1'); - const limit = parseInt(searchParams.get('limit') || '10'); + const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10) || 1); + const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') || '10', 10) || 10));
142-148: Consider adding format validation for email and subdomain.The validation checks for presence but not format. Invalid email addresses or subdomain formats (e.g., special characters, excessive length) would only fail at the database layer, resulting in less informative error messages.
nextjs_space/app/api/tenant-admin/posts/[id]/route.ts (1)
89-99: Potential null reference ifuser.tenantIdisnull.While the change correctly uses
user!.tenantIdinstead ofuser!.tenant!.id, there's a subtle issue: ifuserexists butuser.tenantIdisnull(e.g., for a SUPER_ADMIN without tenant assignment), this query would search for posts wheretenantIdisnull, potentially causing unexpected behavior.Consider adding an explicit guard:
Suggested improvement
// Check collision excluding current post + if (!user?.tenantId) { + return NextResponse.json({ error: 'User has no tenant assigned' }, { status: 403 }); + } while (await prisma.posts.findFirst({ where: { slug: uniqueSlug, - tenantId: user!.tenantId, + tenantId: user.tenantId, NOT: { id } } }) {nextjs_space/lib/encryption.ts (1)
68-71: Migration window allows unencrypted secrets - ensure deadline is enforced.Returning the original text when migration is allowed means unencrypted credentials in the database are accepted. This is a reasonable migration strategy, but ensure:
ENCRYPTION_MIGRATION_DEADLINEis set to a near-future date- A background job encrypts all existing credentials before the deadline
- After the deadline, this path throws an error
Consider logging when unencrypted values are encountered during migration to track progress:
if (parts.length !== 3) { if (isMigrationAllowed(options)) { + console.warn('Decrypting unencrypted value during migration window - this should be encrypted'); return text; }nextjs_space/app/api/reset-password/route.ts (1)
82-84: Remove manual$disconnect()when using the shared Prisma client.After switching to the shared client from
lib/db, thefinallyblock should be removed. The shared client manages its own connection lifecycle.Suggested fix
} catch (error) { console.error('Password reset error:', error); return NextResponse.json( { error: 'Failed to reset password', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ); - } finally { - await prisma.$disconnect(); } }nextjs_space/lib/drgreen-orders.ts (1)
132-143: Consider wrapping remote cart deletion in try-catch.If the Dr. Green cart deletion fails after the transaction commits, the order was still created successfully but the remote cart may persist. This won't affect the user's order but could leave orphaned data on the Dr. Green side. Consider wrapping this in a try-catch to prevent the error from bubbling up and failing the entire request.
♻️ Suggested improvement
if (cart.drGreenCartId) { + try { await callDrGreenAPI( `/dapp/carts/${cart.drGreenCartId}`, { method: 'DELETE', apiKey, secretKey, validateSuccessFlag: true, body: { cartId: cart.drGreenCartId }, } ); + } catch (error) { + console.error('[Order] Failed to clear remote cart:', error); + // Non-fatal: order was created successfully + } }nextjs_space/lib/drgreen-api-client.ts (1)
17-34: Consider logging base64 decode failures for debugging.The empty catch block at lines 24-26 silently ignores decode errors. If the key is neither PEM nor valid base64,
sign.sign()will fail with a less informative error. Consider at least logging for debugging purposes.♻️ Optional improvement
try { privateKeyPEM = Buffer.from(secretKey, 'base64').toString('utf-8'); } catch (error) { - // Keep original key if not base64-encoded. + // Keep original key if not base64-encoded + console.debug('[DrGreen] Key is not base64-encoded, using as-is'); }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (50)
nextjs_space/app/api/auth/reset-password/route.tsnextjs_space/app/api/consultation/submit/route.tsnextjs_space/app/api/onboarding/route.tsnextjs_space/app/api/reset-password/route.tsnextjs_space/app/api/shop/register/route.tsnextjs_space/app/api/signup/route.tsnextjs_space/app/api/super-admin/platform-settings/route.tsnextjs_space/app/api/super-admin/templates/[id]/route.tsnextjs_space/app/api/super-admin/tenants/bulk/route.tsnextjs_space/app/api/super-admin/tenants/route.tsnextjs_space/app/api/tenant-admin/analytics/route.tsnextjs_space/app/api/tenant-admin/orders/bulk/route.tsnextjs_space/app/api/tenant-admin/posts/[id]/route.tsnextjs_space/app/api/tenant-admin/products/bulk/route.tsnextjs_space/app/api/tenant-admin/products/reorder/route.tsnextjs_space/app/api/tenant-admin/select-template/route.tsnextjs_space/app/api/tenant-admin/settings/route.tsnextjs_space/app/api/user/profile/route.tsnextjs_space/app/super-admin/platform-settings/page.tsxnextjs_space/app/super-admin/settings/page.tsxnextjs_space/app/super-admin/templates/page.tsxnextjs_space/app/tenant-admin/branding/page.tsxnextjs_space/app/tenant-admin/settings/page.tsxnextjs_space/app/tenant-admin/settings/settings-form.tsxnextjs_space/components/cookie-consent.tsxnextjs_space/components/tenant-theme-provider.tsxnextjs_space/components/ui/breadcrumb.tsxnextjs_space/components/ui/carousel.tsxnextjs_space/components/ui/dialog.tsxnextjs_space/components/ui/menubar.tsxnextjs_space/components/ui/task-card.tsxnextjs_space/lib/auth.tsnextjs_space/lib/db.tsnextjs_space/lib/doctor-green-api.tsnextjs_space/lib/dr-green-mapping.tsnextjs_space/lib/drgreen-api-client.tsnextjs_space/lib/drgreen-cart.tsnextjs_space/lib/drgreen-orders.tsnextjs_space/lib/encryption.tsnextjs_space/lib/lovable-converter.tsnextjs_space/lib/queue.tsnextjs_space/lib/rate-limit.tsnextjs_space/lib/tenant-config.tsnextjs_space/lib/tenant-context.tsnextjs_space/lib/tenant.tsnextjs_space/lib/webhook.tsnextjs_space/middleware.tsnextjs_space/next.config.jsnextjs_space/prisma/migrations/init_complete_schema.sqlnextjs_space/prisma/schema.prisma
💤 Files with no reviewable changes (1)
- nextjs_space/lib/queue.ts
🧰 Additional context used
🧬 Code graph analysis (29)
nextjs_space/lib/tenant-config.ts (1)
nextjs_space/lib/encryption.ts (1)
decrypt(63-92)
nextjs_space/app/api/tenant-admin/products/reorder/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
checkRateLimit(50-99)nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/api/tenant-admin/products/bulk/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
checkRateLimit(50-99)nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/api/super-admin/tenants/bulk/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
checkRateLimit(50-99)nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/api/reset-password/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/app/api/tenant-admin/orders/bulk/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
checkRateLimit(50-99)nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/lib/rate-limit.ts (1)
nextjs_space/middleware.ts (1)
config(4-16)
nextjs_space/lib/tenant.ts (2)
nextjs_space/lib/tenant-context.ts (1)
setTenantContext(9-11)nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/components/cookie-consent.tsx (1)
nextjs_space/lib/cookie-utils.ts (6)
getConsentModel(52-63)CONSENT_COOKIE_NAME(116-116)CONSENT_CATEGORIES_COOKIE_NAME(117-117)parseConsentCategories(134-148)getDefaultCategories(122-129)stringifyConsentCategories(153-159)
nextjs_space/app/super-admin/settings/page.tsx (1)
nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/api/tenant-admin/settings/route.ts (2)
nextjs_space/lib/encryption.ts (1)
encrypt(20-33)nextjs_space/lib/audit-log.ts (3)
getClientInfo(121-128)createAuditLog(42-62)AUDIT_ACTIONS(67-116)
nextjs_space/app/api/user/profile/route.ts (1)
nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/api/consultation/submit/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/app/api/auth/reset-password/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/app/super-admin/platform-settings/page.tsx (1)
nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/lib/auth.ts (2)
nextjs_space/lib/tenant.ts (1)
getCurrentTenant(14-59)nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/app/api/super-admin/templates/[id]/route.ts (1)
nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/api/onboarding/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/lib/drgreen-orders.ts (2)
nextjs_space/lib/db.ts (1)
prisma(31-37)nextjs_space/lib/drgreen-api-client.ts (1)
callDrGreenAPI(39-91)
nextjs_space/app/tenant-admin/branding/page.tsx (1)
nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/api/signup/route.ts (1)
nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/app/api/super-admin/platform-settings/route.ts (2)
nextjs_space/lib/auth.ts (2)
session(61-68)authOptions(7-77)nextjs_space/lib/db.ts (1)
prisma(31-37)
nextjs_space/app/api/tenant-admin/analytics/route.ts (1)
nextjs_space/lib/rate-limit.ts (1)
checkRateLimit(50-99)
nextjs_space/app/api/tenant-admin/select-template/route.ts (1)
nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/lib/db.ts (1)
nextjs_space/lib/tenant-context.ts (1)
getTenantContext(13-15)
nextjs_space/app/api/shop/register/route.ts (2)
nextjs_space/lib/tenant-config.ts (1)
getTenantDrGreenConfig(10-38)nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/app/super-admin/templates/page.tsx (1)
nextjs_space/lib/auth.ts (1)
session(61-68)
nextjs_space/lib/doctor-green-api.ts (1)
nextjs_space/lib/drgreen-api-client.ts (1)
callDrGreenAPI(39-91)
nextjs_space/app/api/super-admin/tenants/route.ts (3)
nextjs_space/lib/rate-limit.ts (1)
checkRateLimit(50-99)nextjs_space/lib/auth.ts (1)
session(61-68)nextjs_space/lib/db.ts (1)
prisma(31-37)
🪛 ast-grep (0.40.4)
nextjs_space/components/tenant-theme-provider.tsx
[warning] 47-47: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
nextjs_space/components/tenant-theme-provider.tsx
[error] 48-48: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
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 (12)
nextjs_space/lib/encryption.ts (3)
1-3: Duplicate import will cause compilation error.The
cryptomodule is imported twice (lines 1 and 3). Remove one of these imports.Proposed fix
import crypto from "crypto"; - -import crypto from 'crypto';
36-109: Triple redeclaration ofDecryptOptions,DEFAULT_MIGRATION_DEADLINE, andisMigrationAllowed.These declarations appear three times (lines 36-59, 61-84, and 86-109). This will cause TypeScript compilation errors. Keep only one instance of each.
Proposed fix - remove duplicate blocks
-type DecryptOptions = { - allowUnencryptedMigration?: boolean; - migrationDeadline?: string; -}; - -const DEFAULT_MIGRATION_DEADLINE = process.env.ENCRYPTION_MIGRATION_DEADLINE; - -function isMigrationAllowed(options?: DecryptOptions): boolean { - if (!options?.allowUnencryptedMigration) { - return false; - } - - const deadline = options.migrationDeadline ?? DEFAULT_MIGRATION_DEADLINE; - if (!deadline) { - return false; - } - - const deadlineDate = new Date(deadline); - if (Number.isNaN(deadlineDate.getTime())) { - return false; - } - - return Date.now() < deadlineDate.getTime(); -} - -type DecryptOptions = { - allowUnencryptedMigration?: boolean; - migrationDeadline?: string; -}; - -const DEFAULT_MIGRATION_DEADLINE = process.env.ENCRYPTION_MIGRATION_DEADLINE; - -function isMigrationAllowed(options?: DecryptOptions): boolean { - if (!options?.allowUnencryptedMigration) { - return false; - } - - const deadline = options.migrationDeadline ?? DEFAULT_MIGRATION_DEADLINE; - if (!deadline) { - return false; - } - - const deadlineDate = new Date(deadline); - if (Number.isNaN(deadlineDate.getTime())) { - return false; - } - - return Date.now() < deadlineDate.getTime(); -} - type DecryptOptions = { allowUnencryptedMigration?: boolean; migrationDeadline?: string; }; const DEFAULT_MIGRATION_DEADLINE = process.env.ENCRYPTION_MIGRATION_DEADLINE; function isMigrationAllowed(options?: DecryptOptions): boolean { ... }
114-151: Malformeddecryptfunction with redundant format validation blocks.The function contains two overlapping format checks with inconsistent indentation and logic:
- Lines 117–123: Migration-aware check that throws on invalid format
- Lines 125–131: Duplicate check that warns and returns original text (orphaned code from incomplete refactoring)
The
partsvariable is declared twice, and lines 127–130 are unreachable dead code. While the function may execute without crashing, the corrupted structure indicates a failed merge and must be consolidated.The proposed fix removes the redundancy and consolidates to a single validation path. Additionally, consider whether returning
""on decryption failure (line 149) is intentional—masking errors can hide data corruption or key mismatches downstream.nextjs_space/app/api/consultation/submit/route.ts (3)
106-106: Reconsider storing password in ConsultationQuestionnaire.The hashed password is already stored in the
userstable (line 78). Duplicating it in the questionnaire:
- Creates data inconsistency risk if password is updated later
- Expands the credential storage surface unnecessarily
- Mixes authentication data with consultation/medical data
If the password field is required by the schema, consider storing a placeholder or removing it from the model.
272-292: Remove or conditionalize debug logging with PII.This block logs the full payload containing sensitive PII (email, phone, address) and medical records. In production, this creates compliance/privacy risks (GDPR, HIPAA-adjacent data). Consider:
- Removing these logs entirely
- Using structured logging with PII redaction
- Gating behind a debug flag (
if (process.env.DEBUG_DR_GREEN))
296-304: Add timeout to external API call.The
fetchcall to Dr. Green API has no timeout. If the external service is slow or unresponsive, this request will hang indefinitely, potentially exhausting server resources.♻️ Add AbortController with timeout
+ const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout + const response = await fetch(`${drGreenApiUrl}/dapp/clients`, { method: "POST", headers: { "Content-Type": "application/json", "x-auth-apikey": apiKey, "x-auth-signature": signature, }, body: payloadStr, + signal: controller.signal, }); + + clearTimeout(timeoutId);nextjs_space/app/api/tenant-admin/posts/[id]/route.ts (1)
124-165: Critical: Remove duplicate/orphaned code causing syntax errors.Lines 126-165 contain leftover code that must be removed:
Unreachable code (126-154): After the catch block ends at line 124, this code references
validatedData,existingPost, anduserwhich are all declared inside the try block and are out of scope.Orphaned catch (155-164): This
catchblock has no matchingtry, causing a syntax error.Old tenantId pattern (138): Uses
user!.tenant!.idinstead of the correcteduser!.tenantIdthat the PR aims to fix.🐛 Proposed fix: Remove the dead code
console.error('Error updating post:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } - - const dataToUpdate: any = { ...validatedData }; - - // If title changes, regen slug? Optional. Let's do it for SEO. - if (validatedData.title && validatedData.title !== existingPost.title) { - let slug = slugify(validatedData.title); - let uniqueSlug = slug; - let counter = 1; - // Check collision excluding current post - while ( - await prisma.posts.findFirst({ - where: { - slug: uniqueSlug, - tenantId: user!.tenant!.id, - NOT: { id }, - }, - }) - ) { - uniqueSlug = `${slug}-${counter}`; - counter++; - } - dataToUpdate.slug = uniqueSlug; - } - - const updatedPost = await prisma.posts.update({ - where: { id }, - data: dataToUpdate, - }); - - return NextResponse.json(updatedPost); - } catch (error) { - if (error instanceof z.ZodError) { - return NextResponse.json({ error: error.errors }, { status: 400 }); - } - console.error("Error updating post:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } }nextjs_space/lib/drgreen-orders.ts (1)
179-250: Critical: Duplicated and malformed code ingetOrder.This function contains duplicated code blocks with conflicting API call patterns:
- Lines 186-191 use the old positional argument style:
callDrGreenAPI(endpoint, "GET", apiKey, secretKey)- Lines 218-226 use the new object pattern:
callDrGreenAPI(endpoint, { method, apiKey, ... })There's also duplicated logic for "Order not found" checks (lines 179-181 and 211-213) and Dr. Green sync (lines 184-209 and 216-249). This appears to be incomplete merge/refactor remnants.
🐛 Proposed fix: Remove duplicate code, use new API pattern
const order = await prisma.orders.findFirst({ where: { id: orderId, userId, tenantId, }, include: { items: true, }, }); if (!order) { - throw new Error("Order not found"); + throw new Error('Order not found'); } // If order has Dr. Green ID, sync latest status if (order.drGreenOrderId) { try { const drGreenOrder = await callDrGreenAPI( `/dapp/orders/${order.drGreenOrderId}`, - "GET", - apiKey, - secretKey, - ); - - const orderDetails = drGreenOrder.data?.orderDetails; - - if (orderDetails) { - // Update local order with latest Dr. Green status - const updated = await prisma.orders.update({ - where: { id: order.id }, - data: { - // Map Dr. Green payment status to local - paymentStatus: - orderDetails.paymentStatus === "PAID" - ? "PAID" - : order.paymentStatus, - }, - include: { - items: true, - }, - }); - - if (!order) { - throw new Error('Order not found'); - } - - // If order has Dr. Green ID, sync latest status - if (order.drGreenOrderId) { - try { - const drGreenOrder = await callDrGreenAPI( - `/dapp/orders/${order.drGreenOrderId}`, - { - method: 'GET', - apiKey, - secretKey, - validateSuccessFlag: true, - } - ); + { + method: 'GET', + apiKey, + secretKey, + validateSuccessFlag: true, + } + ); - const orderDetails = drGreenOrder.data?.orderDetails; + const orderDetails = drGreenOrder.data?.orderDetails; - if (orderDetails) { - // Update local order with latest Dr. Green status - const updated = await prisma.orders.update({ - where: { id: order.id }, - data: { - // Map Dr. Green payment status to local - paymentStatus: orderDetails.paymentStatus === 'PAID' ? 'PAID' : order.paymentStatus, - }, - include: { - items: true, - }, - }); - - return updated; - } - } catch (error) { - console.error('[Order Sync] Failed to sync with Dr. Green:', error); - // Return local order if sync fails - } + if (orderDetails) { + const updated = await prisma.orders.update({ + where: { id: order.id }, + data: { + paymentStatus: orderDetails.paymentStatus === 'PAID' ? 'PAID' : order.paymentStatus, + }, + include: { + items: true, + }, + }); + return updated; + } + } catch (error) { + console.error('[Order Sync] Failed to sync with Dr. Green:', error); } } - } return order; }nextjs_space/lib/drgreen-cart.ts (3)
152-161: Unreachable code after early return.Lines 152-160 are unreachable because the function returns at line 149 when
cartDataexists. IfcartDatais falsy, line 153 references undefineditemsvariable. This code block should be removed.🐛 Remove unreachable code
return { items, totalQuantity: cartData.totalQuatity || 0, totalAmount: cartData.totalAmount || 0, drGreenCartId: cartData.id, }; - } - - return { - items, - totalQuantity: cartData.totalQuatity || 0, - totalAmount: cartData.totalAmount || 0, - drGreenCartId: cartData.id, - }; - } + } throw new Error("Failed to add item to cart"); }
255-331: Critical: Duplicate and malformedremoveFromCartfunction.This code block contains:
- A duplicate
removeFromCartfunction definition (another exists at lines 335-372)- Old API calling pattern (lines 269-274):
callDrGreenAPI(endpoint, "GET", apiKey, secretKey)- Reference to undefined
cartvariable at line 322This entire block (lines 255-331) appears to be incomplete refactor remnants and should be removed. The correct implementation is at lines 335-372.
🐛 Remove duplicate/malformed function
Remove lines 252-331 entirely. The correct implementation exists at lines 332-372.
361-368: Inconsistent API call pattern inremoveFromCart.This function uses the old positional argument style for
callDrGreenAPIwhile other functions in this file use the new object pattern. Based on thecallDrGreenAPIsignature indrgreen-api-client.ts, the function expects an options object as the second parameter.🐛 Use consistent API call pattern
// Call Dr. Green API to remove item await callDrGreenAPI( `/dapp/carts/${cart.drGreenCartId}?strainId=${strainId}`, - "DELETE", - apiKey, - secretKey, - { cartId: cart.drGreenCartId }, + { + method: 'DELETE', + apiKey, + secretKey, + validateSuccessFlag: true, + body: { cartId: cart.drGreenCartId }, + } );nextjs_space/lib/db.ts (1)
122-139: Upsert handling is incomplete and could allow cross-tenant data access.Several issues with the upsert flow:
Dead code for upsert: Lines 123-131 check
params.args?.data, but upsert operations usecreate/updateproperties, notdata. This code path has no effect on upsert.Missing
whereclause scoping: The early return at line 138 bypasses tenant scoping on the upsert'swhereclause. This could allow matching records from other tenants.Missing
updatedata handling: Theupdateportion of upsert doesn't receive tenant consideration, potentially allowing cross-tenant updates.Proposed fix
if (tenantScopedCreateActions.has(params.action)) { + // Handle upsert separately due to its unique structure + if (params.action === 'upsert') { + // Scope the where clause + if (params.args?.where) { + params.args.where = applyTenantScope(params.args.where, tenantId, allowNull); + } + // Set tenantId on create data + if (params.args?.create) { + params.args.create.tenantId = params.args.create.tenantId ?? tenantId; + } + // Note: update portion intentionally not modified as tenantId should not change + return next(params); + } + + // Handle create/createMany if (params.args?.data) { if (Array.isArray(params.args.data)) { params.args.data = params.args.data.map((item: Record<string, any>) => ({ ...item, tenantId: item.tenantId ?? tenantId, })); } else { params.args.data.tenantId = params.args.data.tenantId ?? tenantId; } } - if (params.action === 'upsert') { - if (params.args?.create) { - params.args.create.tenantId = params.args.create.tenantId ?? tenantId; - } - } return next(params); }
🤖 Fix all issues with AI agents
In @nextjs_space/components/tenant-theme-provider.tsx:
- Around line 1-7: Remove the duplicate "use client" directive at the top of the
module in tenant-theme-provider.tsx so only a single "use client" remains; edit
the module header to keep one directive (either the first or second) and delete
the other to avoid redundant directives while leaving imports and the
Tenant/TenantSettings references intact.
- Around line 289-296: There is a duplicate declaration of the function
sanitizeCustomCss causing a redeclaration error; remove the redundant copy and
keep only the original definition (the one at lines ~280–287), ensure any
imports/uses still resolve to that single function, and run a quick build/TS
check to confirm no references relied on the removed block.
In @nextjs_space/lib/db.ts:
- Around line 70-78: The tenantScopedReadActions Set incorrectly includes the
destructive action 'deleteMany'; remove 'deleteMany' from the
tenantScopedReadActions declaration so that only read-only Prisma actions remain
(e.g., 'findMany','findFirst','findUnique','count','aggregate','groupBy'),
leaving 'deleteMany' only in tenantScopedWriteManyActions where it belongs;
update any related comments/tests if they assert read-vs-write classification
for tenantScopedReadActions or tenantScopedWriteManyActions.
🧹 Nitpick comments (15)
nextjs_space/lib/webhook.ts (1)
122-152:setTimeout-based retries are unreliable in serverless environments.In Next.js API routes (especially serverless/edge deployments), the execution context may terminate after the response is sent. Scheduled
setTimeoutcallbacks may never fire, causing silent retry failures and lost webhook deliveries.Consider using a durable retry mechanism:
- A job queue (e.g., BullMQ with Redis, which aligns with the Redis migration in this PR)
- Database-persisted retry state with a background worker
- External queue service (SQS, Cloud Tasks)
Example: Queue-based retry pattern
// Instead of setTimeout, persist failed deliveries for retry if (!success && attemptCount < 3) { await prisma.webhookDelivery.update({ where: { id: deliveryId }, data: { nextRetryAt: new Date(Date.now() + Math.pow(2, attemptCount) * 1000), status: 'pending_retry', }, }); // A separate cron/worker picks up pending retries }nextjs_space/lib/lovable-converter.ts (3)
478-489: Remove duplicate regex replacements.The same
react-router-domimport removal pattern is applied three times consecutively (lines 478-481, 482-485, 486-489). Since the regex uses the global flag, the first replacement catches all matches, making the subsequent two redundant.♻️ Suggested fix
content = content.replace( /import \{[^}]*\} from ["']react-router-dom["'];?/g, '' ); - content = content.replace( - /import \{[^}]*\} from ["']react-router-dom["'];?/g, - '' - ); - content = content.replace( - /import \{[^}]*\} from ["']react-router-dom["'];?/g, - '' - );
68-73: Consider narrowing the error type.Using
anyfor the caught error loses type safety. While this works, TypeScript'sunknowntype is preferred for catch blocks.♻️ Suggested fix
- } catch (error: any) { + } catch (error: unknown) { console.error("[Lovable Converter] Error:", error); return { success: false, - error: error.message || "Unknown conversion error", + error: error instanceof Error ? error.message : "Unknown conversion error", }; }
272-272: Type annotations useany.
generateDefaultsFilereturnsPromise<any>andgenerateTemplateConfigreturnsany. Defining explicit interfaces for these return types would improve maintainability and catch potential mismatches at compile time.Also applies to: 341-341
nextjs_space/app/api/consultation/submit/route.ts (2)
56-91: Consider hashing password only when creating a new user.
bcrypt.hashwith 10 rounds is computationally expensive. Currently, the password is hashed on line 61 before checking if the user exists (lines 64-69). For returning users, this work is wasted sincehashedPasswordis only used inprisma.users.create.♻️ Suggested reordering
export async function POST(request: NextRequest) { try { const body = await request.json(); - // Hash the password before storing - const hashedPassword = await bcrypt.hash(body.password, 10); - // Check if user already exists const existingUser = await prisma.users.findFirst({ where: { email: body.email.toLowerCase(), ...(body.tenantId ? { tenantId: body.tenantId } : {}), }, }); let userId: string | undefined; // Create user account if doesn't exist if (!existingUser) { + // Hash the password only when creating a new user + const hashedPassword = await bcrypt.hash(body.password, 10); const newUser = await prisma.users.create({Note: You'll need to also hash the password for the questionnaire creation on line 106, so consider extracting a helper or restructuring the flow accordingly.
315-320: Consider reducing error log verbosity.Logging full response headers (line 319) in error scenarios is helpful for debugging but may expose infrastructure details in logs. For production, consider logging only essential fields like
statusandstatusText.nextjs_space/app/api/tenant-admin/posts/[id]/route.ts (1)
99-108: Consider adding an iteration limit to the slug uniqueness loop.While unlikely in practice, the
whileloop has no upper bound. If there's unexpected data or a bug, this could run excessively. Consider adding a safeguard:♻️ Suggested improvement
let uniqueSlug = slug; let counter = 1; + const maxAttempts = 100; // Check collision excluding current post while (await prisma.posts.findFirst({ where: { slug: uniqueSlug, tenantId: user!.tenantId, NOT: { id } } })) { + if (counter >= maxAttempts) { + return NextResponse.json({ error: 'Unable to generate unique slug' }, { status: 500 }); + } uniqueSlug = `${slug}-${counter}`; counter++; }nextjs_space/components/cookie-consent.tsx (3)
4-4: Unused import:Shield.The
Shieldicon is imported but not used anywhere in the component.🔧 Suggested fix
-import { Cookie, Settings, Shield } from 'lucide-react'; +import { Cookie, Settings } from 'lucide-react';
42-56: RedundantgetConsentModelcall.
getConsentModel(tenant.countryCode)is already computed at line 34 asconsentModel. The duplicate call at line 44 is unnecessary.Note: The empty dependency array will likely trigger an ESLint
react-hooks/exhaustive-depswarning due to thetenant.countryCodereference.🔧 Suggested fix
useEffect(() => { setMounted(true); - const model = getConsentModel(tenant.countryCode); // Check if consent has already been given const existingConsent = getCookieValue(CONSENT_COOKIE_NAME); const existingCategories = getCookieValue(CONSENT_CATEGORIES_COOKIE_NAME); const parsed = parseConsentCategories(existingCategories); - setCategories(parsed || getDefaultCategories(model)); + setCategories(parsed || getDefaultCategories(consentModel)); // Only show banner if consent hasn't been given yet if (!existingConsent || existingConsent !== 'true') { setShowBanner(true); } - }, []); + }, [consentModel]);
105-108: Consider URL-encoding the cookie value.The JSON string from
stringifyConsentCategoriescontains characters ({,},:,") that are not valid in cookie values per RFC 6265. While most browsers handle this, URL-encoding the value would be more robust and prevent potential parsing issues with proxies or edge cases.🔧 Suggested fix
const saveCategories = (cats: ConsentCategories) => { const secureFlag = window.location.protocol === 'https:' ? '; Secure' : ''; - document.cookie = `${CONSENT_CATEGORIES_COOKIE_NAME}=${stringifyConsentCategories(cats)}; path=/; max-age=31536000; SameSite=Lax${secureFlag}`; + document.cookie = `${CONSENT_CATEGORIES_COOKIE_NAME}=${encodeURIComponent(stringifyConsentCategories(cats))}; path=/; max-age=31536000; SameSite=Lax${secureFlag}`; };You'll also need to update
getCookieValueto decode:function getCookieValue(name: string): string | undefined { if (typeof document === "undefined") return undefined; const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)")); return match ? decodeURIComponent(match[2]) : undefined; }nextjs_space/lib/doctor-green-api.ts (2)
84-88: Dead code: warning is unreachable.Line 88's warning can never execute because the function throws on line 85 when
!apiKey. Remove this unreachable statement.🧹 Remove dead code
if (!apiKey || !secretKey) { throw new Error("MISSING_CREDENTIALS"); } - if (!apiKey) console.warn("Warning: No Dr Green API Key provided"); - return callDrGreenAPI(endpoint, {
403-431: Inconsistent body handling: pre-stringified vs object.
createClientpasses a pre-stringified JSON body (line 408), while all other API calls in this file pass objects directly. AlthoughcallDrGreenAPIhandles both cases, this inconsistency reduces maintainability.♻️ Pass object directly for consistency
const response = await doctorGreenRequest<{ clientId: string; kycLink?: string; }>("/clients", { method: "POST", - body: JSON.stringify({ + body: { first_name: clientData.firstName, last_name: clientData.lastName, email: clientData.email, phone: clientData.phone, date_of_birth: clientData.dateOfBirth, address: { street: clientData.address.street, city: clientData.address.city, postal_code: clientData.address.postalCode, country: clientData.address.country, }, medical_record: clientData.medicalRecord ? { conditions: clientData.medicalRecord.conditions, current_medications: clientData.medicalRecord.currentMedications, allergies: clientData.medicalRecord.allergies, previous_cannabis_use: clientData.medicalRecord.previousCannabisUse, doctor_approval: clientData.medicalRecord.doctorApproval, } : undefined, - }), + }, config, });nextjs_space/lib/drgreen-orders.ts (2)
67-84: Add type annotation for API response.
callDrGreenAPIreturns an untyped response here. Add a generic type parameter to ensure type safety when accessingdrGreenResponse.data.♻️ Add type annotation
+interface DrGreenOrderApiResponse { + data?: { + id: string; + invoiceNumber?: string; + }; +} // Submit order to Dr. Green API - const drGreenResponse = await callDrGreenAPI( + const drGreenResponse = await callDrGreenAPI<DrGreenOrderApiResponse>( '/dapp/orders', {
132-143: Remote cart deletion outside transaction may leave inconsistent state.If the remote cart deletion fails (lines 132-143), the local order and cart deletion have already been committed. Consider:
- Moving this call inside the transaction (though it won't rollback the remote call on failure)
- Or wrapping in try-catch to log failures without throwing, since the order is already created
♻️ Add error handling for remote cart deletion
if (cart.drGreenCartId) { + try { await callDrGreenAPI( `/dapp/carts/${cart.drGreenCartId}`, { method: 'DELETE', apiKey, secretKey, validateSuccessFlag: true, body: { cartId: cart.drGreenCartId }, } ); + } catch (error) { + console.error('[Order Submit] Failed to clear remote cart:', error); + // Non-critical: order was created successfully + } }nextjs_space/lib/db.ts (1)
9-18: Mock client is missing some operations now referenced in action sets.The mock Prisma client doesn't include
aggregate,groupBy,updateMany, ordeleteManymethods that are now referenced in the tenant scoping logic. If any build-time code paths use these methods, they'll receiveundefined.Suggested addition to mock client
const createMockPrismaClient = (): any => { const mockModel = { findUnique: async () => null, findFirst: async () => null, findMany: async () => [], create: async () => ({}), update: async () => ({}), delete: async () => ({}), count: async () => 0, + aggregate: async () => ({}), + groupBy: async () => [], + updateMany: async () => ({ count: 0 }), + deleteMany: async () => ({ count: 0 }), + createMany: async () => ({ count: 0 }), + upsert: async () => ({}), };
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
nextjs_space/app/api/consultation/submit/route.tsnextjs_space/app/api/super-admin/platform-settings/route.tsnextjs_space/app/api/super-admin/tenants/route.tsnextjs_space/app/api/tenant-admin/posts/[id]/route.tsnextjs_space/app/api/tenant-admin/settings/route.tsnextjs_space/components/cookie-consent.tsxnextjs_space/components/tenant-theme-provider.tsxnextjs_space/components/ui/breadcrumb.tsxnextjs_space/components/ui/carousel.tsxnextjs_space/components/ui/dialog.tsxnextjs_space/components/ui/menubar.tsxnextjs_space/components/ui/task-card.tsxnextjs_space/lib/db.tsnextjs_space/lib/doctor-green-api.tsnextjs_space/lib/dr-green-mapping.tsnextjs_space/lib/drgreen-cart.tsnextjs_space/lib/drgreen-orders.tsnextjs_space/lib/encryption.tsnextjs_space/lib/lovable-converter.tsnextjs_space/lib/queue.tsnextjs_space/lib/webhook.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- nextjs_space/components/ui/breadcrumb.tsx
- nextjs_space/components/ui/dialog.tsx
- nextjs_space/lib/dr-green-mapping.ts
- nextjs_space/lib/queue.ts
- nextjs_space/components/ui/carousel.tsx
- nextjs_space/app/api/super-admin/tenants/route.ts
- nextjs_space/app/api/tenant-admin/settings/route.ts
🧰 Additional context used
🧬 Code graph analysis (6)
nextjs_space/app/api/super-admin/platform-settings/route.ts (2)
nextjs_space/lib/auth.ts (2)
session(61-68)authOptions(7-77)nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/components/cookie-consent.tsx (1)
nextjs_space/lib/cookie-utils.ts (7)
ConsentCategories(57-62)getConsentModel(76-93)CONSENT_COOKIE_NAME(148-148)CONSENT_CATEGORIES_COOKIE_NAME(149-149)parseConsentCategories(166-182)getDefaultCategories(154-161)stringifyConsentCategories(187-195)
nextjs_space/lib/drgreen-orders.ts (2)
nextjs_space/lib/db.ts (1)
prisma(34-43)nextjs_space/lib/drgreen-api-client.ts (1)
callDrGreenAPI(39-91)
nextjs_space/app/api/tenant-admin/posts/[id]/route.ts (2)
nextjs_space/lib/auth.ts (2)
session(61-68)authOptions(7-77)nextjs_space/lib/db.ts (1)
prisma(34-43)
nextjs_space/lib/drgreen-cart.ts (2)
nextjs_space/lib/db.ts (1)
prisma(34-43)nextjs_space/lib/drgreen-api-client.ts (1)
callDrGreenAPI(39-91)
nextjs_space/lib/doctor-green-api.ts (1)
nextjs_space/lib/drgreen-api-client.ts (1)
callDrGreenAPI(39-91)
🪛 ast-grep (0.40.4)
nextjs_space/components/tenant-theme-provider.tsx
[warning] 53-53: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
nextjs_space/components/tenant-theme-provider.tsx
[error] 54-54: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
[error] 289-289: Shouldn't redeclare 'sanitizeCustomCss'. Consider to delete it or rename it.
'sanitizeCustomCss' is defined here:
(lint/suspicious/noRedeclare)
nextjs_space/lib/encryption.ts
[error] 86-86: Shouldn't redeclare 'DecryptOptions'. Consider to delete it or rename it.
'DecryptOptions' is defined here:
(lint/suspicious/noRedeclare)
[error] 91-91: Shouldn't redeclare 'DEFAULT_MIGRATION_DEADLINE'. Consider to delete it or rename it.
'DEFAULT_MIGRATION_DEADLINE' is defined here:
(lint/suspicious/noRedeclare)
[error] 93-93: Shouldn't redeclare 'isMigrationAllowed'. Consider to delete it or rename it.
'isMigrationAllowed' is defined here:
(lint/suspicious/noRedeclare)
[error] 118-118: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
🔇 Additional comments (21)
nextjs_space/components/ui/menubar.tsx (1)
217-217: LGTM!Correct fix: React's
displayNamestatic property must use camelCase for React DevTools to recognize and display the component name properly during debugging.nextjs_space/components/ui/task-card.tsx (2)
1-6: LGTM!Clean style normalization: removed unused
useStateimport and standardized quote style to single quotes. These changes align with the broader PR's code style conventions.
66-66: Minor formatting change looks fine.The closing parenthesis formatting is consistent with standard practices.
nextjs_space/components/tenant-theme-provider.tsx (1)
40-40: Good addition of CSS sanitization before injection.The memoized
sanitizeCustomCssproperly strips@import,url(), andexpression()patterns before injecting viadangerouslySetInnerHTML. This mitigates the primary CSS injection vectors (external resource loading, legacy IE expressions).While static analysis flags
dangerouslySetInnerHTMLusage, this is an acceptable pattern for CSS injection when sanitization is applied. Consider whether additional vectors need blocking (e.g.,@font-face,@namespace, or-moz-bindingfor legacy Firefox), though modern browsers have largely mitigated these.Also applies to: 53-55
nextjs_space/lib/webhook.ts (1)
189-196: LGTM! Proper length guard before timing-safe comparison.The length check before
crypto.timingSafeEqualis the correct approach—it prevents the function from throwing when buffer lengths differ while maintaining timing attack resistance for equal-length inputs. Since the expected signature length (64 hex chars for SHA256) is deterministic, the early return doesn't leak sensitive information.nextjs_space/lib/lovable-converter.ts (3)
450-450: LGTM!The refined regex
/<Link\b([^>]*?)\s+to=/gcorrectly targets theto=attribute while preserving other attributes on the<Link>component. This handles the React Router to Next.js migration properly.
499-499: Consistent Link replacement.This follows the same pattern as line 450, maintaining consistency across the Header refactoring logic.
572-572: Consistent Link replacement in Footer.Matches the same pattern used in
refactorComponentFileandrefactorHeaderComponent, ensuring consistent handling of the React Router to Next.js Link migration.nextjs_space/app/api/consultation/submit/route.ts (1)
184-193: Address mapping fix looks correct.The change to use
body.addressLine2aligns with the field names used in the questionnaire storage (lines 108-109) and the form data structure. This should resolve the shipping data mapping issue.nextjs_space/app/api/super-admin/platform-settings/route.ts (1)
111-123: LGTM! Authorization correctly added to GET handler.The authorization pattern properly mirrors the POST handler, ensuring only authenticated SUPER_ADMIN users can access platform settings. The 401/403 response codes are appropriate for the respective authentication and authorization failures.
nextjs_space/app/api/tenant-admin/posts/[id]/route.ts (1)
74-123: New PATCH implementation logic looks correct.The authorization, validation, tenant ownership check, and slug regeneration logic are properly implemented. Using
user!.tenantId(line 102) is the correct approach per the session callback inlib/auth.ts.nextjs_space/components/cookie-consent.tsx (4)
27-32: LGTM!State and ref declarations are appropriate for the modal-based cookie consent flow with focus management.
58-62: LGTM!Proper focus management for accessibility—focusing the first interactive element when the modal opens.
115-139: Good focus trap implementation.The keyboard handling for Escape and Tab cycling is correctly implemented for accessibility.
One enhancement to consider: when the modal closes (via Escape, Cancel, or Save), focus should return to the element that triggered the modal (the "customize" button) to maintain keyboard navigation context. This would require storing a ref to the trigger button and calling
.focus()on close.
141-159: LGTM!Modal accessibility is well implemented with proper ARIA attributes (
role="dialog",aria-modal="true",aria-labelledby), backdrop click-to-close, and keyboard event handling on the container. The structure follows accessibility best practices.nextjs_space/lib/doctor-green-api.ts (1)
6-8: Clean delegation to the centralized API client.The refactor correctly delegates to
callDrGreenAPIwhile preserving the existing interface. The credential validation before delegation is appropriate as it provides a clearer error context.Also applies to: 90-97
nextjs_space/lib/drgreen-orders.ts (1)
40-64: Validation logic is well-structured.Good defensive checks: verifying user has completed consultation (drGreenClientId) and cart is not empty before making the API call. This prevents unnecessary API calls and provides clear error messages.
nextjs_space/lib/drgreen-cart.ts (2)
166-250: Well-structured cart retrieval with good error handling.The
getCartfunction properly handles the case where a user hasn't completed consultation (lines 241-247), returning an empty cart instead of throwing. The local cart sync viaupsertis appropriate.Note:
totalQuatity(lines 227, 146, etc.) appears to be a typo in the Dr. Green API response. Consider adding a comment to document this external API quirk for maintainability.
377-415: Clean implementation ofclearCart.Correctly checks for existing cart before making API call, and cleans up local state afterward.
nextjs_space/lib/db.ts (2)
80-89: Action sets correctly categorized.The separation of write-many and create actions is well-structured. However,
upsertrequires careful handling as it combines both create and update semantics with awhereclause—see related comment on the middleware implementation.
141-150: Tenant scoping for single update/delete operations is properly enforced at the route level.While the middleware does not auto-scope single-record
updateanddeleteoperations (onlyupdateMany/deleteMany), the route handlers that use these operations validate tenant ownership viafindFirstorfindUniquebefore performing the modification. For example, webhook and post deletion routes verifytenantIdmatches the user's tenant before executing the operation. This design choice is intentional—centralized middleware handles common many-operations, while route handlers manage single-record operations (which naturally require a lookup first). No cross-tenant modification vulnerability exists in the current implementation.Likely an incorrect or invalid review comment.
| function sanitizeCustomCss(css?: string | null): string { | ||
| if (!css) return ''; | ||
|
|
||
| return css | ||
| .replace(/@import[^;]+;/gi, '') | ||
| .replace(/url\(([^)]+)\)/gi, '') | ||
| .replace(/expression\(([^)]+)\)/gi, ''); | ||
| } |
There was a problem hiding this comment.
Critical: Remove duplicate sanitizeCustomCss function declaration.
This function is already defined at lines 280-287. The duplicate declaration at lines 289-296 will cause a build error or runtime exception. As noted by static analysis, this redeclaration must be removed.
Proposed fix
-
-function sanitizeCustomCss(css?: string | null): string {
- if (!css) return '';
-
- return css
- .replace(/@import[^;]+;/gi, '')
- .replace(/url\(([^)]+)\)/gi, '')
- .replace(/expression\(([^)]+)\)/gi, '');
-}📝 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.
| function sanitizeCustomCss(css?: string | null): string { | |
| if (!css) return ''; | |
| return css | |
| .replace(/@import[^;]+;/gi, '') | |
| .replace(/url\(([^)]+)\)/gi, '') | |
| .replace(/expression\(([^)]+)\)/gi, ''); | |
| } |
🧰 Tools
🪛 Biome (2.1.2)
[error] 289-289: Shouldn't redeclare 'sanitizeCustomCss'. Consider to delete it or rename it.
'sanitizeCustomCss' is defined here:
(lint/suspicious/noRedeclare)
🤖 Prompt for AI Agents
In @nextjs_space/components/tenant-theme-provider.tsx around lines 289 - 296,
There is a duplicate declaration of the function sanitizeCustomCss causing a
redeclaration error; remove the redundant copy and keep only the original
definition (the one at lines ~280–287), ensure any imports/uses still resolve to
that single function, and run a quick build/TS check to confirm no references
relied on the removed block.
Motivation
Description
shipping.address2mapping to usebody.addressLine2inapp/api/consultation/submit/route.tsand exportedmapMedicalConditionsForDrGreento a shared module used by the route.GEThandler inapp/api/super-admin/platform-settings/route.ts, updated several handlers to look up users byid, and corrected usages offindUnique→findFirstwhere appropriate.app/api/super-admin/tenants/route.tsto type the Prisma transaction (Prisma.TransactionClient) and use the correct model names (tenants,users,tenant_branding) inside the transaction, and fixed tenant ID lookup inapp/api/tenant-admin/posts/[id]/route.tsto useuser!.tenantIdfor slug uniqueness checks.lib/tenant-context.ts, enhancedlib/tenant.tsto set context, and added Prisma middleware inlib/db.tsto automatically apply tenant scoping and defaulttenantIdon create operations.lib/drgreen-api-client.ts, updatedlib/doctor-green-api.ts,lib/drgreen-cart.ts, andlib/drgreen-orders.tsto use the new client and validate responses.checkRateLimitasynchronous inlib/rate-limit.ts, and added other infra improvements such asgetEmailQueuecleanup, encryption improvements inlib/encryption.ts, and tenant credential decryption fixes inlib/tenant-config.ts.components/cookie-consent.tsxwith ARIA attributes, Escape/backdrop close, focus trap and secure cookie flag; addedsanitizeCustomCssincomponents/tenant-theme-provider.tsx; and several small UI/utility fixes (displayName typos, unsubscribing event listeners, masking API keys in settings UI, etc.).Testing
Codex Task
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Refactor
Style
Chores
✏️ Tip: You can customize this high-level summary in your review settings.