Skip to content

Clear tenant context on errors; add tenant-scoping, Redis rate limiter, and secrets handling#2

Merged
AutomatosAI merged 5 commits intomainfrom
codex/perform-full-review-of-saas-platform-x7zylq
Jan 13, 2026
Merged

Clear tenant context on errors; add tenant-scoping, Redis rate limiter, and secrets handling#2
AutomatosAI merged 5 commits intomainfrom
codex/perform-full-review-of-saas-platform-x7zylq

Conversation

@AutomatosAI
Copy link
Copy Markdown
Owner

@AutomatosAI AutomatosAI commented Jan 13, 2026

Motivation

  • Prevent stale tenant IDs from persisting across requests when tenant lookup fails or throws.
  • Ensure all database operations are automatically scoped to the current tenant to avoid cross-tenant data leaks.
  • Replace the in-memory rate limiter with a Redis-backed fixed-window limiter suitable for multi-instance deployments.
  • Improve handling and encryption of secret credentials (Dr. Green and SMTP) and avoid shipping plaintext keys to the client.

Description

  • Add a request-scoped tenant store (nextjs_space/lib/tenant-context.ts) and clear it on errors by calling setTenantContext(null) in getTenantFromRequest/getCurrentTenant.
  • Implement automatic tenant scoping in Prisma via middleware in nextjs_space/lib/db.ts so reads/writes are annotated with tenantId and creation defaults to the current tenant.
  • Replace in-memory limiter with a Redis-backed fixed-window limiter in nextjs_space/lib/rate-limit.ts, make checkRateLimit/getRateLimitStatus asynchronous, and switch call sites to await checkRateLimit(...).
  • Miscellaneous fixes and hardening: normalize/lowercase emails and switch many findUnique to findFirst, change session lookups to use session.user.id, require ENCRYPTION_KEY in lib/encryption.ts, decrypt tenant Dr. Green keys via getTenantDrGreenConfig, mask API keys in settings UI, and add audit logging on tenant settings updates; also update Prisma schema unique indexes to be tenant-scoped.

Testing

  • No automated tests were executed for these changes.
  • No CI runs or test-suite executions were performed as part of this rollout.
  • Changes were validated by running the code edits and basic local commits (no test output available).
  • Manual code inspection and logging were used to verify the tenant context clearing and Redis rate-limit logic during development.

Codex Task

Summary by CodeRabbit

  • New Features

    • Tenant context propagation and tenant-scoped data enforcement across requests.
    • Audit logging for tenant settings updates capturing client metadata (IP, user agent).
  • Bug Fixes

    • Rate limiting migrated to Redis for reliable, consistent throttling.
    • Improved API key UI: masking, verification placeholders, and clearer guidance.
  • Security

    • Stronger encryption key sourcing and masking of sensitive keys in logs.
    • Email/order uniqueness enforced per-tenant via composite constraints.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

Introduces multi-tenant enforcement and context propagation (AsyncLocalStorage), converts Prisma reads to tenant-aware findFirst via middleware, moves rate limiting to an async Redis-backed implementation, adjusts encryption/key handling and masking, and updates various API routes and pages to use tenant- or id-based lookups and audit logging.

Changes

Cohort / File(s) Summary
Prisma Tenant Middleware & Schema
nextjs_space/lib/db.ts, nextjs_space/prisma/schema.prisma, nextjs_space/prisma/migrations/init_complete_schema.sql
Added Prisma $use tenant-scoping middleware (auto-apply tenantId, convert findUnique→findFirst for scoped reads) and updated schema to composite unique indexes on (tenantId, email) and (tenantId, orderNumber).
Tenant Context & Routing / Middleware
nextjs_space/lib/tenant-context.ts, nextjs_space/lib/tenant.ts, nextjs_space/middleware.ts
New AsyncLocalStorage tenant context API, path-based /store/{slug} resolution, setTenantContext usage, and middleware header sanitization/propagation (x-tenant-*) for routing.
Rate Limiting → Redis (Async)
nextjs_space/lib/rate-limit.ts, nextjs_space/app/api/super-admin/tenants/bulk/route.ts, nextjs_space/app/api/super-admin/tenants/route.ts, nextjs_space/app/api/tenant-admin/analytics/route.ts, nextjs_space/app/api/tenant-admin/orders/bulk/route.ts, nextjs_space/app/api/tenant-admin/products/bulk/route.ts, nextjs_space/app/api/tenant-admin/products/reorder/route.ts
Replaced in-memory sliding-window limiter with Redis-backed fixed-window limiter; API now async (await checkRateLimit(...)) and exposes getRateLimitStatus async helpers; added Redis client initialization and TTL-based reset handling.
Email Lookup Semantics (findUnique → findFirst)
nextjs_space/app/api/auth/reset-password/route.ts, nextjs_space/app/api/onboarding/route.ts, nextjs_space/app/api/signup/route.ts, nextjs_space/app/api/super-admin/tenants/route.ts
Switched various email-based queries from findUnique to findFirst to align with tenant-scoped schema and middleware.
Tenant-Aware User Resolution & ID-based Updates
nextjs_space/app/api/consultation/submit/route.ts, nextjs_space/app/api/reset-password/route.ts, nextjs_space/app/api/shop/register/route.ts, nextjs_space/app/api/shop/register/...
Added case-insensitive email lookup, optional tenantId filtering, and switched some update operations to target users by id instead of email; tenant-specific Dr. Green config lookup added with fallback to env creds.
Switches from Email → ID for Authorization Lookups
nextjs_space/app/api/super-admin/platform-settings/route.ts, nextjs_space/app/api/super-admin/templates/[id]/route.ts, nextjs_space/app/api/tenant-admin/select-template/route.ts, nextjs_space/app/api/user/profile/route.ts, nextjs_space/app/super-admin/platform-settings/page.tsx, nextjs_space/app/super-admin/settings/page.tsx, nextjs_space/app/super-admin/templates/page.tsx, nextjs_space/app/tenant-admin/branding/page.tsx
Replaced queries using session.user.email with session.user.id (authorization checks and user resolution).
Auth Tenant Awareness
nextjs_space/lib/auth.ts
Auth lookup now uses findFirst and includes current tenantId when available; includes related tenants and orderBy.
Encryption / Tenant Config
nextjs_space/lib/encryption.ts, nextjs_space/lib/tenant-config.ts
Encryption key source restricted to ENCRYPTION_KEY (removed NEXT_PUBLIC fallback). Tenant config now decrypts and validates both Dr. Green API key and secret.
Tenant Settings, Audit Logging & API Key Masking
nextjs_space/app/api/tenant-admin/settings/route.ts, nextjs_space/app/tenant-admin/settings/page.tsx, nextjs_space/app/tenant-admin/settings/settings-form.tsx
Added createAuditLog calls with client metadata, conditional encryption of drGreenApiKey before storage, masked API key display (********) and adjusted form placeholder/UX to not pre-fill keys.
Next.js Config Change
nextjs_space/next.config.js
images.unoptimized now conditional on env var NEXT_IMAGE_UNOPTIMIZED instead of always true.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Middleware
  participant TenantContext
  participant RateLimiter as Redis
  participant AppServer as API
  participant PrismaDB as Database

  Client->>Middleware: HTTP request (headers, path)
  Middleware->>TenantContext: determine tenant (slug/subdomain/custom-domain) & set header
  Middleware->>AppServer: forward request with sanitized x-tenant-* headers
  AppServer->>RateLimiter: await checkRateLimit(session.user.id)
  RateLimiter-->>AppServer: allow / reject (with Retry-After)
  AppServer->>PrismaDB: tenant-scoped query (middleware applies tenantId, findFirst)
  PrismaDB-->>AppServer: result (user/tenant/data)
  AppServer-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 In burrows deep the tenant hops,

Headers clean and keys that lock,
Redis counts the hopping stops,
Context flows along each clock,
A rabbit cheers: “Scoped code — on the clock!” 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: clearing tenant context on errors, adding tenant-scoping to database operations, implementing a Redis-based rate limiter, and improving secrets handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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/tenant-admin/branding/page.tsx (1)

10-15: Guard check doesn't validate the field being used.

The guard on line 10 checks for session?.user?.email, but line 15 now queries by session.user.id. If id is missing from the session (e.g., token didn't populate it), the query will fail or return no results.

Update the guard to also check for id:

Proposed fix
-  if (!session?.user?.email) {
+  if (!session?.user?.email || !session.user.id) {
     redirect('/auth/login');
   }
nextjs_space/prisma/schema.prisma (1)

437-458: Tenant-scoped email uniqueness is appropriate for multi-tenancy.

The composite @@unique([tenantId, email]) correctly enables the same email across different tenants while preventing duplicates within a tenant.

Production authentication in lib/auth.ts correctly implements tenant-aware email lookups. However, scripts/debug-auth.ts has a bug at lines 15-16: it performs a findUnique lookup by email without tenant context (where: { email }), which bypasses the unique constraint. While this is a debug script and won't affect production, it should be fixed to use findFirst with tenant filtering to match the production pattern.

As noted for the migration file, ensure NULL tenantId users (super-admins) are handled correctly at the application layer, since PostgreSQL won't enforce email uniqueness among them.

nextjs_space/app/api/reset-password/route.ts (2)

6-6: Use shared Prisma client from @/lib/db instead of creating a new instance.

Creating a new PrismaClient per request:

  1. Bypasses the tenant-scoping middleware added in lib/db.ts
  2. Creates unnecessary database connections (connection pool exhaustion risk)
  3. Is inconsistent with other routes in this PR
🔧 Suggested fix
-import { PrismaClient } from '@prisma/client';
+import { prisma } from '@/lib/db';
...
-const prisma = new PrismaClient();

Also remove the prisma.$disconnect() in the finally block (line 83) as the shared client manages its own lifecycle.


24-58: Hardcoded test data and unexpected user creation in password reset endpoint.

This password reset endpoint creates a new user with hardcoded values (name: 'Gerard Kavanagh', tenant: 'healingbuds') when the user doesn't exist. This is problematic:

  1. Password reset should not create users - return 404 instead
  2. Hardcoded name and tenant are clearly debug/test artifacts
  3. Security risk: allows account enumeration and unauthorized account creation
🔧 Suggested fix
     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 NextResponse.json(
+        { error: 'User not found' },
+        { status: 404 }
+      );
     }
nextjs_space/lib/tenant.ts (1)

152-171: Missing setTenantContext(null) in subdomain fallthrough path.

If the subdomain check at line 152 passes but findFirst returns null (tenant not found/inactive), execution falls through to line 171 without clearing the tenant context. Add setTenantContext(null) before the final return:

🔧 Suggested fix
       if (tenant) {
         setTenantContext(tenant.id);
         return tenant;
       }
     }
   } catch (error) {
     console.error('Error fetching tenant from request:', error);
     setTenantContext(null);
     return null;
   }
 
+  setTenantContext(null);
   return null;
 }
nextjs_space/app/api/super-admin/platform-settings/route.ts (1)

12-18: Guard checks email but query uses id — potential type mismatch.

Line 12 validates session?.user?.email, but line 18 queries by session.user.id. While the auth setup populates both, this creates two issues:

  1. Type safety: session.user.id is accessed via as any cast in auth.ts, so TypeScript won't catch if id is undefined.
  2. Semantic mismatch: If email exists but id is somehow missing, findUnique({ where: { id: undefined } }) will throw a Prisma error.

Consider updating the guard to check what you actually use:

-    if (!session?.user?.email) {
+    if (!session?.user?.id) {

Or check both if email is needed elsewhere in the flow.

nextjs_space/app/super-admin/settings/page.tsx (1)

9-16: Missing authOptions parameter may cause session.user.id to be undefined.

getServerSession() is called without authOptions, but the custom session properties (id, role, tenantId) are only populated by the callbacks defined in authOptions. Without passing it, the session may only contain the default next-auth properties (email, name, image).

This would cause line 16 to query with id: undefined, which will throw a Prisma validation error.

-    const session = await getServerSession();
+    const session = await getServerSession(authOptions);

Also requires importing authOptions:

import { authOptions } from '@/lib/auth';

Additionally, the guard on line 11 checks email but line 16 uses id — consider aligning the check with the usage.

nextjs_space/app/super-admin/platform-settings/page.tsx (1)

9-16: Missing authOptionssession.user.id will be undefined.

Same issue as nextjs_space/app/super-admin/settings/page.tsx: getServerSession() is called without authOptions, so the custom session properties (id, role, tenantId) won't be populated.

+import { authOptions } from '@/lib/auth';
...
-  const session = await getServerSession();
+  const session = await getServerSession(authOptions);

Without this fix, session.user.id will be undefined, and prisma.users.findUnique({ where: { id: undefined } }) will fail.

nextjs_space/app/api/signup/route.ts (1)

47-57: Tenant-scoping missing in email duplicate check.

The query checks for existing users across all tenants, but the user is created with tenantId: tenant.id. This prevents the same email from being used across different tenants, which contradicts the multi-tenant model implied by the composite unique constraint on (tenantId, email) in the schema.

🐛 Proposed fix
     // Check if user already exists
     const existingUser = await prisma.users.findFirst({
-      where: { email },
+      where: { 
+        email,
+        tenantId: tenant.id,
+      },
     });
nextjs_space/app/api/shop/register/route.ts (1)

77-83: Add validation for session.user.id before database update.

The authorization check at line 13 validates session?.user?.email, but the update at line 79 uses session.user.id without validation. While the auth flow should populate both together, a defensive check prevents potential Prisma errors if the token is corrupted or incomplete.

♻️ Suggested defensive check
-    if (!session?.user?.email) {
+    if (!session?.user?.email || !session?.user?.id) {
       return NextResponse.json(
         { error: 'Unauthorized' },
         { status: 401 }
       );
     }
nextjs_space/app/api/auth/reset-password/route.ts (1)

18-28: Inconsistent tenant handling in password reset lookup.

The email lookup uses findFirst without tenant context, unlike the login flow which filters by tenant. Since the schema allows duplicate emails across tenants (@@unique([tenantId, email])), this may reset the password for the wrong user if the same email exists in multiple tenants. The reset token is sent without explicit tenant confirmation, creating potential confusion. Align with the login pattern by:

  1. Extracting tenant context (already available via headers, see lib/tenant.ts)
  2. Filtering email lookup by both email and tenantId
  3. Or clarify if platform-wide password resets are intentional
nextjs_space/app/api/user/profile/route.ts (1)

10-15: Auth check is inconsistent with the lookup field.

The authorization check validates session?.user?.email but the update uses session.user.id. If id is missing from the session but email exists, the update will fail. Consider checking for id instead.

🐛 Suggested fix
-        if (!session?.user?.email) {
+        if (!session?.user?.id) {
             return NextResponse.json(
                 { error: 'Unauthorized' },
                 { status: 401 }
             );
         }
nextjs_space/app/api/tenant-admin/select-template/route.ts (1)

12-17: Auth check is inconsistent with the lookup field.

Same pattern as the profile route: validates session?.user?.email but then queries by session.user.id at line 20. If the session has email but not id, the query will fail.

🐛 Suggested fix
-    if (!session?.user?.email) {
+    if (!session?.user?.id) {
       return NextResponse.json(
         { error: 'Unauthorized' },
         { status: 401 }
       );
     }
nextjs_space/app/api/super-admin/tenants/route.ts (2)

162-171: Clarify email uniqueness intent and add ordering or filtering.

The users table has a composite unique constraint on (tenantId, email), meaning the same email can exist across different tenants. The current findFirst lookup on email alone is non-deterministic and unclear in intent:

  • If checking tenant-scoped uniqueness: filter by the current tenant's ID
  • If checking global uniqueness: this contradicts the schema design
  • Either way: add an orderBy clause (e.g., orderBy: { createdAt: 'asc' }) to ensure deterministic results in case of edge cases with NULL tenantIds

178-211: Critical: Transaction uses incorrect model names that don't match the schema.

The transaction code uses singular and camelCase model names (tx.tenant, tx.user, tx.tenantBranding) but your Prisma schema defines them in plural form: tenants, users, tenant_branding. Update the transaction to use the correct model names:

tx.tenants.create(...)
tx.users.create(...)
tx.tenant_branding.create(...)

Prisma delegates (inside transactions) use the exact model names as defined in the schema, not variations. These incorrect names will cause runtime errors.

🤖 Fix all issues with AI agents
In @nextjs_space/lib/auth.ts:
- Around line 20-26: When calling prisma.users.findFirst in the authentication
path, add an orderBy clause for the branch where tenant?.id is falsy so matching
by email only returns deterministically (e.g., orderBy: { createdAt: 'asc' } or
{ tenantId: 'asc' }); update the query that uses getCurrentTenant() and
prisma.users.findFirst (the branch that uses credentials.email and include: {
tenants: true }) to include orderBy to prevent nondeterministic selection when
multiple users share the same email across tenants.

In @nextjs_space/lib/db.ts:
- Around line 131-133: The upsert update branch should not modify tenant
ownership; remove the assignment to params.args.update.tenantId (or at minimum
avoid setting it during the update path) and instead ensure tenantId is applied
only on creation and enforced via the query filter (use
params.args.create.tenantId = params.args.create.tenantId ?? tenantId and ensure
the where clause includes tenantId). Update the code references: stop changing
params.args.update.tenantId, set tenantId only on params.args.create, and verify
the upsert where/filter uses tenantId to prevent cross-tenant updates.
- Around line 112-114: The middleware unconditionally rewrites params.action
from 'findUnique' to 'findFirst' (params.action = 'findFirst') which is unsafe
for models without composite unique constraints; update the logic to only
rewrite for models that actually have a tenant-scoped unique constraint (i.e.,
check the model name against the tenantScopedModels list or a new allowlist of
models with @@unique including tenantId) or alternatively add composite @@unique
constraints including tenantId to the affected models (audit_logs,
consultations, drgreen_webhook_logs, email_logs, email_templates, webhooks), so
that only models guaranteed unique per tenant are converted to findFirst.

In @nextjs_space/lib/rate-limit.ts:
- Around line 59-70: The current rate-limit logic resets the TTL on every
request because pexpire(key, windowMs) is called unconditionally inside the
redis multi, turning fixed-window into sliding-window behavior; change the logic
so pexpire is only set when the key is first created (i.e., when the increment
result shows count === 1). Concretely, keep using getRedisClient(),
redis.multi() and .incr(key) but remove the unconditional .pexpire from the
multi and instead call .pexpire(key, windowMs) only when the computed count
(from results?.[0]?.[1] or the local count variable) equals 1; leave ttlMs/pttl
and retryAfter calculation as-is.

In @nextjs_space/prisma/migrations/init_complete_schema.sql:
- Line 407: The composite unique index users_tenantId_email_key does not prevent
duplicate emails when tenantId IS NULL; add a partial unique index on
users(email) WHERE tenantId IS NULL (e.g., users_email_global_key) to enforce
uniqueness for platform/global users; update the migration to create that
partial index and ensure any rollback/drop uses the same index name so DB-level
uniqueness is enforced for NULL tenantId rows.
🧹 Nitpick comments (5)
nextjs_space/app/super-admin/templates/page.tsx (1)

15-21: Guard check doesn't validate the field used in query.

Line 15 checks for session?.user?.email, but the query on line 20 uses session.user.id. Since id is a custom property added via the session callback (not part of NextAuth's default types), there's a type-safety gap—if id is somehow missing, this query could fail or return unexpected results.

Consider updating the guard to check for id:

- if (!session?.user?.email) {
+ if (!session?.user?.id) {
    redirect('/auth/login');
  }
nextjs_space/lib/db.ts (1)

64-77: deleteMany appears in both action sets - consider consolidating.

deleteMany is included in both tenantScopedReadActions (line 71) and tenantScopedWriteManyActions (line 76). While functionally harmless (both apply where-clause filtering), deleteMany is semantically a write operation. Consider removing it from tenantScopedReadActions for clarity.

nextjs_space/app/api/onboarding/route.ts (1)

60-70: Consider normalizing email before lookup to ensure consistent duplicate detection.

The switch to findFirst is appropriate for the multi-tenant model. However, the email lookup doesn't normalize the input (e.g., lowercase), which could allow bypassing duplicate detection if emails are stored with different casing.

♻️ Suggested fix
+    const normalizedEmail = email.toLowerCase().trim();
+
     // Check if email already exists
     const existingUser = await prisma.users.findFirst({
-      where: { email },
+      where: { email: normalizedEmail },
     });

Also ensure email is normalized when creating the user at line 141.

nextjs_space/app/api/shop/register/route.ts (1)

48-53: Consider early return when credentials are missing.

The code logs an error but continues even with missing credentials, deferring failure to createClient. An early return with a clear error message would provide better UX and avoid unnecessary downstream calls.

♻️ Suggested improvement
     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: 'Dr. Green API is not configured for this tenant' },
+        { status: 503 }
+      );
     }
nextjs_space/lib/rate-limit.ts (1)

12-20: Redis client lacks connection error handling and lifecycle management.

The lazy-initialized Redis client doesn't handle connection errors or provide a way to gracefully shut down. Consider adding error event handlers and an optional closeRedisClient function for graceful shutdown.

Additionally, maxRetriesPerRequest: null causes infinite retries on failed commands, which could block requests indefinitely during Redis outages.

♻️ Suggested improvement
 const getRedisClient = () => {
   if (!redisClient) {
     redisClient = new Redis(REDIS_URL, {
-      maxRetriesPerRequest: null,
+      maxRetriesPerRequest: 3,
       lazyConnect: true,
+      retryStrategy: (times) => Math.min(times * 50, 2000),
     });
+    redisClient.on('error', (err) => {
+      console.error('[RateLimit] Redis connection error:', err);
+    });
   }
   return redisClient;
 };
+
+export const closeRedisClient = async () => {
+  if (redisClient) {
+    await redisClient.quit();
+    redisClient = null;
+  }
+};
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95ecc8c and ed24090.

📒 Files selected for processing (34)
  • nextjs_space/app/api/auth/reset-password/route.ts
  • nextjs_space/app/api/consultation/submit/route.ts
  • nextjs_space/app/api/onboarding/route.ts
  • nextjs_space/app/api/reset-password/route.ts
  • nextjs_space/app/api/shop/register/route.ts
  • nextjs_space/app/api/signup/route.ts
  • nextjs_space/app/api/super-admin/platform-settings/route.ts
  • nextjs_space/app/api/super-admin/templates/[id]/route.ts
  • nextjs_space/app/api/super-admin/tenants/bulk/route.ts
  • nextjs_space/app/api/super-admin/tenants/route.ts
  • nextjs_space/app/api/tenant-admin/analytics/route.ts
  • nextjs_space/app/api/tenant-admin/orders/bulk/route.ts
  • nextjs_space/app/api/tenant-admin/products/bulk/route.ts
  • nextjs_space/app/api/tenant-admin/products/reorder/route.ts
  • nextjs_space/app/api/tenant-admin/select-template/route.ts
  • nextjs_space/app/api/tenant-admin/settings/route.ts
  • nextjs_space/app/api/user/profile/route.ts
  • nextjs_space/app/super-admin/platform-settings/page.tsx
  • nextjs_space/app/super-admin/settings/page.tsx
  • nextjs_space/app/super-admin/templates/page.tsx
  • nextjs_space/app/tenant-admin/branding/page.tsx
  • nextjs_space/app/tenant-admin/settings/page.tsx
  • nextjs_space/app/tenant-admin/settings/settings-form.tsx
  • nextjs_space/lib/auth.ts
  • nextjs_space/lib/db.ts
  • nextjs_space/lib/encryption.ts
  • nextjs_space/lib/rate-limit.ts
  • nextjs_space/lib/tenant-config.ts
  • nextjs_space/lib/tenant-context.ts
  • nextjs_space/lib/tenant.ts
  • nextjs_space/middleware.ts
  • nextjs_space/next.config.js
  • nextjs_space/prisma/migrations/init_complete_schema.sql
  • nextjs_space/prisma/schema.prisma
🧰 Additional context used
🧬 Code graph analysis (25)
nextjs_space/app/api/tenant-admin/select-template/route.ts (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/api/super-admin/platform-settings/route.ts (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/api/super-admin/tenants/route.ts (3)
nextjs_space/lib/rate-limit.ts (1)
  • checkRateLimit (50-97)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/lib/db.ts (1)
  • prisma (31-37)
nextjs_space/app/api/super-admin/tenants/bulk/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
  • checkRateLimit (50-97)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/api/signup/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-97)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/api/tenant-admin/analytics/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
  • checkRateLimit (50-97)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/api/tenant-admin/products/reorder/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
  • checkRateLimit (50-97)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/super-admin/templates/page.tsx (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/api/reset-password/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (31-37)
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/auth/reset-password/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (31-37)
nextjs_space/app/api/tenant-admin/products/bulk/route.ts (2)
nextjs_space/lib/rate-limit.ts (1)
  • checkRateLimit (50-97)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/api/super-admin/templates/[id]/route.ts (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/tenant-admin/branding/page.tsx (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/lib/db.ts (1)
nextjs_space/lib/tenant-context.ts (1)
  • getTenantContext (13-15)
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/lib/tenant-config.ts (1)
nextjs_space/lib/encryption.ts (1)
  • decrypt (38-67)
nextjs_space/app/api/shop/register/route.ts (3)
nextjs_space/middleware.ts (1)
  • config (4-16)
nextjs_space/lib/tenant-config.ts (1)
  • getTenantDrGreenConfig (10-38)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/app/super-admin/platform-settings/page.tsx (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
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/super-admin/settings/page.tsx (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
nextjs_space/lib/rate-limit.ts (1)
nextjs_space/middleware.ts (1)
  • config (4-16)
nextjs_space/app/api/consultation/submit/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (31-37)
nextjs_space/app/api/user/profile/route.ts (1)
nextjs_space/lib/auth.ts (1)
  • session (60-67)
🔇 Additional comments (35)
nextjs_space/next.config.js (1)

17-17: LGTM!

The strict equality check against 'true' is the correct pattern for boolean-like environment variables. Image optimization remains enabled by default when the env var is unset, which is sensible behavior.

nextjs_space/app/tenant-admin/branding/page.tsx (1)

14-23: ID-based lookup is correct and properly typed.

The switch from email-based to ID-based user lookup is correct and consistent with the broader tenant-context refactor. The next-auth types are already properly augmented in nextjs_space/types/next-auth.d.ts with id: string on both the User and Session interfaces, so session.user.id has full TypeScript support.

nextjs_space/lib/encryption.ts (1)

5-5: Good security hardening.

Removing the NEXT_PUBLIC_ENCRYPTION_KEY fallback is the right call—NEXT_PUBLIC_* environment variables are bundled into client-side JavaScript in Next.js, which would have exposed the encryption key to browsers.

nextjs_space/prisma/migrations/init_complete_schema.sql (1)

416-416: LGTM for tenant-scoped order numbers.

The composite unique constraint on (tenantId, orderNumber) correctly ensures order numbers are unique within each tenant while allowing the same order number across different tenants.

nextjs_space/prisma/schema.prisma (1)

232-248: LGTM – tenant-scoped order uniqueness.

The composite unique constraint on (tenantId, orderNumber) is the correct approach for multi-tenant order isolation. This ensures order numbers are unique per tenant.

nextjs_space/lib/tenant-context.ts (1)

1-19: Well-implemented request-scoped tenant context.

The use of AsyncLocalStorage is appropriate for propagating tenant context across async boundaries in Next.js server-side code. The dual API (setTenantContext for imperative use, runWithTenantContext for callback-scoped use) covers common patterns.

A brief JSDoc on each export would help maintainers understand the distinction (e.g., enterWith modifies the current context vs run creates an isolated scope), but this is optional.

nextjs_space/lib/tenant-config.ts (1)

27-36: LGTM! Proper decryption validation for both credentials.

The change to decrypt the API key (in addition to the secret key) and validate both decrypted values before returning is a solid security improvement. The falsy check correctly catches decrypt failures since decrypt() returns an empty string on error.

nextjs_space/lib/db.ts (1)

103-151: Well-structured tenant-scoping middleware.

The middleware correctly:

  • Guards registration with '$use' in prisma check
  • Falls through when no tenant context or model isn't scoped
  • Handles both read filtering and write defaulting appropriately
  • Supports null-tenant access for shared templates
nextjs_space/app/api/consultation/submit/route.ts (1)

78-83: LGTM! Tenant-aware user lookup with email normalization.

The change from findUnique to findFirst with optional tenant scoping is correct for multi-tenant scenarios. Email lowercasing ensures consistent lookups regardless of input case.

nextjs_space/app/api/reset-password/route.ts (1)

20-22: The findFirst and id-based update changes are correct.

Using findFirst for user lookup and updating by user.id instead of email aligns with the multi-tenant approach where email alone may not be unique across tenants.

Also applies to: 63-66

nextjs_space/lib/tenant.ts (2)

21-22: LGTM! Consistent tenant context management on all exit paths.

The pattern of calling setTenantContext(null) on early exits and errors, and setTenantContext(tenant?.id ?? null) on success ensures the context is always in a known state. This prevents stale tenant IDs from persisting across requests.

Also applies to: 52-52, 56-57


131-145: Good addition of path-based tenant resolution.

The /store/{slug} path matching complements the existing subdomain logic and aligns with getTenantUrl() which generates these URLs. Setting tenant context immediately upon match ensures downstream code has access to the tenant ID.

nextjs_space/app/api/super-admin/templates/[id]/route.ts (1)

14-24: LGTM — correctly uses authOptions.

Unlike the page components, this API route correctly passes authOptions to getServerSession(), ensuring session.user.id is populated.

Minor note: The guard on line 16 checks email while line 24 uses id. For consistency with the actual usage, consider updating the guard:

-    if (!session?.user?.email) {
+    if (!session?.user?.id) {

This is a minor inconsistency rather than a bug since authOptions ensures both are populated together.

nextjs_space/middleware.ts (2)

22-26: Good security hardening: Sanitizing incoming tenant headers.

Stripping x-tenant-slug, x-tenant-subdomain, and x-tenant-custom-domain from incoming requests prevents clients from spoofing tenant context. This is a solid defense-in-depth measure for multi-tenant isolation.


40-93: LGTM!

The consistent use of NextResponse.next({ request: { headers: requestHeaders } }) across all routing paths ensures tenant headers are properly propagated (or sanitized) downstream. The routing priority (path → subdomain → custom domain → platform) is logically sound.

nextjs_space/app/api/auth/reset-password/route.ts (1)

46-57: LGTM!

The email sending logic appropriately falls back to 'SYSTEM' for the tenantId when the user doesn't have one. The await with catch pattern ensures the response isn't blocked by email failures.

nextjs_space/app/api/shop/register/route.ts (1)

34-46: Good defensive pattern for tenant credential resolution.

The fallback to platform environment credentials when tenant-specific config fails is a sensible approach. The warning log helps with debugging without breaking the flow.

nextjs_space/lib/rate-limit.ts (1)

106-141: LGTM!

The getRateLimitStatus function correctly handles edge cases: returns safe defaults on Redis errors and properly handles TTL values. The fail-open strategy aligns with checkRateLimit.

nextjs_space/app/api/user/profile/route.ts (1)

33-42: LGTM!

The switch from email-based to ID-based lookup is a correct improvement. Using the primary key (id) for updates is more efficient and stable than using email, which can change.

nextjs_space/app/api/super-admin/tenants/route.ts (2)

25-28: LGTM!

The rate limit check is correctly awaited to match the new async signature.


125-128: LGTM!

Correctly awaits the async checkRateLimit call.

nextjs_space/app/api/tenant-admin/analytics/route.ts (1)

17-21: LGTM!

The rate limit check is correctly awaited to match the new async Redis-backed implementation.

nextjs_space/app/api/tenant-admin/products/reorder/route.ts (1)

19-23: LGTM!

The rate limit check is correctly awaited.

nextjs_space/app/api/tenant-admin/products/bulk/route.ts (1)

30-34: LGTM!

The rate limit check is correctly awaited to match the new async signature.

nextjs_space/app/api/tenant-admin/select-template/route.ts (1)

19-22: LGTM!

The switch from email-based to ID-based lookup is correct and aligns with the PR's broader pattern of using stable identifiers for user lookups.

nextjs_space/app/api/super-admin/tenants/bulk/route.ts (2)

30-34: LGTM! Rate limit check correctly awaited.

The migration to the async Redis-backed rate limiter is properly implemented. The await ensures the Promise resolves before checking success.


76-96: Good audit logging implementation.

The audit logs capture relevant context including previous/new status, bulk operation flag, and batch size. Using crypto.randomUUID() for IDs is appropriate.

nextjs_space/app/api/tenant-admin/orders/bulk/route.ts (2)

32-36: LGTM! Async rate limiting properly integrated.

The await is correctly added for the Redis-backed rate limiter, and the success check pattern remains consistent with other endpoints.


71-97: Good tenant isolation pattern.

The code correctly enforces tenant boundaries by filtering orders on tenantId during both fetch and update operations, preventing cross-tenant data access.

nextjs_space/app/tenant-admin/settings/page.tsx (1)

22-28: LGTM! Consistent API key masking.

Both drGreenSecretKey and drGreenApiKey are now properly masked before being passed to the client component, preventing sensitive credential exposure in the browser.

nextjs_space/app/tenant-admin/settings/settings-form.tsx (2)

29-34: LGTM! Proper handling of masked API key in form state.

The hasApiKey flag correctly detects whether a key exists (the masked '********' value is truthy), and initializing drGreenApiKey to empty string prevents accidentally submitting the mask placeholder back to the server.


156-160: Good UX for credential management.

The conditional placeholder and helper text provide clear guidance—users see "Leave empty to keep existing key" when a key is already configured, avoiding confusion about whether to re-enter credentials.

nextjs_space/app/api/tenant-admin/settings/route.ts (3)

82-89: LGTM! Consistent encryption for API key.

The drGreenApiKey encryption follows the same pattern as drGreenSecretKey, only encrypting when a non-empty value is provided. This preserves existing keys when the form field is left empty.


103-117: Good audit logging implementation.

The audit log captures relevant metadata including updated field names (not values), SMTP update status, and client information. Field names like drGreenApiKey appearing in updatedFields is acceptable since only the key names are logged, not the actual values.


91-95: Good practice: sensitive keys masked in logs.

The console output correctly masks drGreenSecretKey and drGreenApiKey values, preventing accidental credential exposure in server logs while still providing useful debugging information about which fields were updated.

Comment thread nextjs_space/lib/db.ts
Comment thread nextjs_space/lib/db.ts Outdated
Comment thread nextjs_space/prisma/migrations/init_complete_schema.sql
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed24090292

ℹ️ 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".

Comment thread nextjs_space/app/api/auth/reset-password/route.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @nextjs_space/lib/db.ts:
- Around line 116-133: The upsert handling is incomplete: ensure the upsert's
lookup and update are tenant-scoped and avoid the early return that skips read
scoping; specifically, when params.action === 'upsert' set
params.args.where.tenantId = params.args.where?.tenantId ?? tenantId and also
set params.args.update.tenantId = params.args.update?.tenantId ?? tenantId (in
addition to the existing params.args.create handling), and move or modify the
return logic so the subsequent read-scoping logic that scopes params.args.where
for other actions still runs or is applied to the upsert case as well before
calling next(params).
- Around line 64-77: The middleware is missing tenant scoping for single-record
write actions: add 'update' and 'delete' to the tenant-scoped write set (or
create a new tenantScopedWriteSingleActions = new Set(['update','delete'])) and
remove the duplicate 'deleteMany' from tenantScopedReadActions; then update the
request handling in the Prisma middleware (the function that inspects
action/params) to inject or merge the tenant filter into params.where for single
writes (e.g., ensure params.where = { AND: [params.where || {}, { tenantId:
currentTenantId }] }) and validate/throw if a write would run without a tenant
constraint so single-record update/delete cannot bypass tenant scoping.

In @nextjs_space/lib/rate-limit.ts:
- Around line 12-20: The Redis client in getRedisClient currently sets
maxRetriesPerRequest: null and lazyConnect: true which can cause indefinite
hangs when Redis is down; update the Redis constructor options for redisClient
to use a bounded retry policy (e.g., set maxRetriesPerRequest to a finite number
or supply a retryStrategy), add a connection timeout (e.g., connectTimeout or
socket.connectTimeout), and disable offline queueing (enableOfflineQueue: false)
so commands fail fast; ensure getRedisClient (and any caller) handles connection
errors from lazyConnect() or explicitly connect with a timeout and surface the
error instead of letting requests hang.
- Around line 59-72: The INCR and PEXPIRE are separate calls causing a
non-atomic race where a crash after INCR can leave the key without a TTL; change
the pipeline in the rate limiter (where getRedisClient() is used and
multi().incr(key).exec() is called) to include PEXPIRE in the same MULTI/EXEC
and also request PTTL in that same transaction so you can parse the incremented
count and TTL from the single exec() result; remove the separate await
redis.pexpire(...) and await redis.pttl(...) calls and instead extract count and
ttlMs from the multi exec response, using the existing key and windowMs values.
🧹 Nitpick comments (1)
nextjs_space/lib/rate-limit.ts (1)

94-98: Consider returning rate limit headers on successful requests.

Currently, X-RateLimit-* headers are only returned when rate limited (429). Returning these headers on success would allow clients to monitor their usage proactively.

Suggested enhancement
-  return { success: true };
+  return { 
+    success: true as const,
+    headers: {
+      'X-RateLimit-Limit': maxRequests.toString(),
+      'X-RateLimit-Remaining': Math.max(0, maxRequests - count).toString(),
+      'X-RateLimit-Reset': new Date(now + ttlMs).toISOString(),
+    }
+  };

Note: This would require updating callers to propagate these headers.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed24090 and 9295e9c.

📒 Files selected for processing (3)
  • nextjs_space/lib/auth.ts
  • nextjs_space/lib/db.ts
  • nextjs_space/lib/rate-limit.ts
🧰 Additional context used
🧬 Code graph analysis (3)
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/lib/db.ts (1)
nextjs_space/lib/tenant-context.ts (1)
  • getTenantContext (13-15)
nextjs_space/lib/rate-limit.ts (1)
nextjs_space/middleware.ts (1)
  • config (4-16)
🔇 Additional comments (5)
nextjs_space/lib/auth.ts (1)

4-4: LGTM!

Import added to support tenant-aware authentication flow.

nextjs_space/lib/db.ts (3)

1-2: LGTM!

Import of getTenantContext correctly added to support the new tenant-scoping middleware.


85-101: LGTM!

The applyTenantScope helper correctly handles both strict tenant scoping and the null-access variant for shared templates. Overwriting any existing tenantId in the spread is actually secure—it prevents callers from bypassing tenant isolation.


135-147: LGTM!

The read and write-many action scoping logic correctly applies tenant constraints to the where clause, handling both existing and missing where conditions.

nextjs_space/lib/rate-limit.ts (1)

108-143: LGTM!

The implementation correctly handles concurrent Redis queries and provides sensible defaults on errors. The TTL fallback logic properly addresses edge cases where the key doesn't exist or has no expiry.

Comment thread nextjs_space/lib/auth.ts
Comment thread nextjs_space/lib/db.ts Outdated
Comment thread nextjs_space/lib/db.ts Outdated
Comment on lines +12 to +20
const getRedisClient = () => {
if (!redisClient) {
redisClient = new Redis(REDIS_URL, {
maxRetriesPerRequest: null,
lazyConnect: true,
});
}
}, 5 * 60 * 1000);
return redisClient;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Risk of indefinite request hangs with current Redis configuration.

Setting maxRetriesPerRequest: null disables the per-request retry limit, causing commands to retry indefinitely if Redis is unavailable. Combined with lazyConnect: true and no explicit timeouts, this can cause API requests to hang forever when Redis is down.

Consider adding connection timeouts and a bounded retry strategy:

Suggested fix
 const getRedisClient = () => {
   if (!redisClient) {
     redisClient = new Redis(REDIS_URL, {
-      maxRetriesPerRequest: null,
+      maxRetriesPerRequest: 3,
+      connectTimeout: 5000,
+      commandTimeout: 2000,
       lazyConnect: true,
     });
+    redisClient.on('error', (err) => {
+      console.error('[RateLimit] Redis connection error:', err);
+    });
   }
   return redisClient;
 };
🤖 Prompt for AI Agents
In @nextjs_space/lib/rate-limit.ts around lines 12 - 20, The Redis client in
getRedisClient currently sets maxRetriesPerRequest: null and lazyConnect: true
which can cause indefinite hangs when Redis is down; update the Redis
constructor options for redisClient to use a bounded retry policy (e.g., set
maxRetriesPerRequest to a finite number or supply a retryStrategy), add a
connection timeout (e.g., connectTimeout or socket.connectTimeout), and disable
offline queueing (enableOfflineQueue: false) so commands fail fast; ensure
getRedisClient (and any caller) handles connection errors from lazyConnect() or
explicitly connect with a timeout and surface the error instead of letting
requests hang.

Comment thread nextjs_space/lib/rate-limit.ts
@AutomatosAI AutomatosAI merged commit d5cda42 into main Jan 13, 2026
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 13, 2026
@AutomatosAI AutomatosAI deleted the codex/perform-full-review-of-saas-platform-x7zylq branch January 13, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant