Conversation
- add dashboard - add create api key
📝 WalkthroughWalkthroughThis PR adds Logto-based authentication across frontend and backend, implements API key generation/validation and per-user rate limiting, splits food data into natural and halal collections, introduces dashboard and API docs UI, and adds ESLint/Prettier tooling plus related config, migrations, and seeds. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Browser
participant Frontend
participant Logto
participant Backend
participant Database
rect rgb(210,230,255)
Note over User,Logto: Authentication (Sign In) Flow
User->>Browser: Click "Sign In"
Browser->>Frontend: Navigate /callback (via Logto redirect)
Frontend->>Backend: Callback -> handleSignIn (code)
Backend->>Logto: Exchange code for token / fetch claims
Logto-->>Backend: Return claims (sub, email, name)
Backend->>Database: Upsert user by logtoId
Database-->>Backend: User persisted
Backend-->>Frontend: Set session cookie
Frontend->>Browser: Redirect to /login-success
Browser-->>User: Show countdown then redirect to /dashboard
end
sequenceDiagram
autonumber
actor User
participant Browser
participant Frontend as Dashboard
participant TRPC
participant Backend
participant Database
rect rgb(220,255,220)
Note over User,Database: API Key Generation & Revoke
User->>Browser: Open Generate Key modal
Browser->>TRPC: Call apiKeys.generate(name, expiration)
TRPC->>Backend: apiKeysRouter.generate
Backend->>Backend: Create plain key, hash with SHA-256
Backend->>Database: Insert api_keys doc (hash, prefix, meta)
Database-->>Backend: Persisted
Backend-->>TRPC: Return { plainKey, publicMeta }
TRPC-->>Browser: Show plainKey once (copy)
User->>Browser: Click Revoke
Browser->>TRPC: Call apiKeys.revoke(keyId)
TRPC->>Backend: Revoke handler
Backend->>Database: Update isRevoked=true
Database-->>Backend: Success
Backend-->>TRPC: Confirm revoke
TRPC-->>Browser: Refresh list
end
sequenceDiagram
autonumber
actor User
participant Browser
participant Frontend
participant TRPC
participant Backend
participant Database
rect rgb(255,240,220)
Note over User,Database: Dual-Tab Food Search (natural vs halal)
User->>Browser: Select "Halal" tab
Browser->>TRPC: Call halal.brands() and halal.search(query)
TRPC->>Backend: Route to halalRouter
Backend->>Database: Query halal_foods (filters/pagination)
Database-->>Backend: Results
Backend-->>TRPC: Mapped DTOs (with brand/cert)
TRPC-->>Browser: Render halal results
User->>Browser: Scroll for more
Browser->>TRPC: halal.allPaginated(nextCursor, brand?)
TRPC->>Backend: Paginate query
Backend->>Database: Return next page
Database-->>Backend: Page results
Backend-->>TRPC: Return items + nextCursor
TRPC-->>Browser: Append items
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/kal-backend/src/routers/api.ts (1)
124-124: Avoid dynamicimport()forObjectId—use static import.Dynamic imports add overhead on each request. Since
ObjectIdis used in multiple routes, import it statically at the top of the file.🔎 Suggested fix
import { Router, type Router as RouterType } from "express"; +import { ObjectId } from "mongodb"; import { getDB } from "../lib/db.js"; import { validateApiKeyMiddleware } from "../middleware/api-key-middleware.js"; // Then in the route handlers, remove: - const { ObjectId } = await import("mongodb");This applies to both lines 124 and 331.
🧹 Nitpick comments (35)
packages/kal-frontend/src/app/search/page.tsx (5)
45-50: Consider resetting search state when switching tabs.When the user switches tabs,
inputValueandsearchQueryare not reset. This means a search query from the "natural" tab will persist and immediately apply to the "halal" tab (and vice versa), which may yield unexpected results or no results at all.🔎 Proposed fix
// Reset filters when switching tabs useEffect(() => { setSelectedCategory(""); setSelectedBrand(""); setSelectedFood(null); + setInputValue(""); + setSearchQuery(""); }, [activeTab]);
196-218: Consider adding accessibility attributes to tab buttons.The tab buttons function as toggles but lack
aria-pressedor a proper tab role to indicate the selected state for screen reader users.🔎 Proposed fix
<button onClick={() => setActiveTab("natural")} + aria-pressed={activeTab === "natural"} className={`px-6 py-3 rounded-xl font-medium transition-all duration-200 ${activeTab === "natural" ? "bg-accent text-dark shadow-lg shadow-accent/20" : "bg-dark-surface text-content-secondary border border-dark-border hover:border-accent/30" }`} > 🍚 Natural Foods </button> <button onClick={() => setActiveTab("halal")} + aria-pressed={activeTab === "halal"} className={`px-6 py-3 rounded-xl font-medium transition-all duration-200 ${activeTab === "halal" ? "bg-emerald-500 text-white shadow-lg shadow-emerald-500/20" : "bg-dark-surface text-content-secondary border border-dark-border hover:border-emerald-500/30" }`} > Halal Certified </button>
360-414: Consider using a proper type guard instead of property checks.The
"brand" in foodcheck is used as a discriminator, but this is fragile—if aFooditem ever includes abrandproperty, the logic will break. Additionally, the repeated(food as HalalFood)casts are verbose.A type guard function would improve both type safety and readability:
🔎 Proposed refactor
Add a type guard function at the top of the component or in a utils file:
function isHalalFood(food: Food | HalalFood): food is HalalFood { return "halalCertifier" in food || "halalCertYear" in food; }Then use it in the render logic:
- {foods.map((food) => { - const isHalalFood = "brand" in food; + {foods.map((food) => { + const halalFood = isHalalFood(food) ? food : null; return ( <button ... - {isHalalFood && (food as HalalFood).brand && ( - <span className="..."> - {(food as HalalFood).brand} + {halalFood?.brand && ( + <span className="..."> + {halalFood.brand} </span> )}
495-512: Minor indentation inconsistency in modal.Line 507 has inconsistent indentation compared to the surrounding JSX. This doesn't affect functionality but makes the code structure harder to follow.
🔎 Proposed fix
{selectedFood.category && ( <span className="text-xs text-content-muted"> {selectedFood.category} </span> )} </div> - {"brand" in selectedFood && (selectedFood as HalalFood).halalCertifier && ( - <p className="text-xs text-emerald-400 mt-2"> - {(selectedFood as HalalFood).halalCertifier} Certified - {(selectedFood as HalalFood).halalCertYear && ` (${(selectedFood as HalalFood).halalCertYear})`} - </p> + {"brand" in selectedFood && (selectedFood as HalalFood).halalCertifier && ( + <p className="text-xs text-emerald-400 mt-2"> + {(selectedFood as HalalFood).halalCertifier} Certified + {(selectedFood as HalalFood).halalCertYear && ` (${(selectedFood as HalalFood).halalCertYear})`} + </p> )}
484-520: Consider adding keyboard support to close the modal.The modal can only be closed by clicking the close button or selecting a different food item. Adding support for the
Escapekey would improve keyboard accessibility.🔎 Proposed enhancement
Add a
useEffectto handle the Escape key:useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape" && selectedFood) { setSelectedFood(null); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [selectedFood]);packages/kal-frontend/src/components/sign-in.tsx (1)
1-16: Add error handling and loading state to the SignIn component.The component is actively used in the navbar with server actions. Adding error handling and loading state would improve the user experience by providing feedback during the sign-in process and gracefully handling any failures.
🔎 Suggested improvement with error handling and loading state
'use client'; + +import { useState } from 'react'; type Props = { onSignIn: () => Promise<void>; }; export default function SignIn({ onSignIn }: Props) { + const [loading, setLoading] = useState(false); + + const handleSignIn = async () => { + try { + setLoading(true); + await onSignIn(); + } catch (error) { + console.error('Sign in failed:', error); + } finally { + setLoading(false); + } + }; + return ( <button - onClick={() => onSignIn()} + onClick={handleSignIn} + disabled={loading} className="sign-in-btn" > - Sign In + {loading ? 'Signing in...' : 'Sign In'} </button> ); }packages/kal-frontend/src/app/api-docs/client.tsx (2)
158-174: Shared response state may cause UX confusion.The
responseanderrorstate is shared across all endpoints. When a user tries one endpoint, then expands another, the previous response remains visible. Consider keying the response byactiveEndpointor clearing it when a different endpoint is expanded.🔎 Proposed approach
const tryEndpoint = async (example: string) => { setLoading(true); setError(null); setResponse(null); + // Store which endpoint this response is for + const currentEndpoint = activeEndpoint; try { const apiUrl = getApiUrl(); const fullUrl = apiUrl ? `${apiUrl}${example}` : example; const res = await fetch(fullUrl); const data = await res.json(); - setResponse(JSON.stringify(data, null, 2)); + // Only update if still viewing the same endpoint + if (currentEndpoint === activeEndpoint) { + setResponse(JSON.stringify(data, null, 2)); + } } catch (err) { - setError(err instanceof Error ? err.message : "Failed to fetch"); + if (currentEndpoint === activeEndpoint) { + setError(err instanceof Error ? err.message : "Failed to fetch"); + } } finally { setLoading(false); } };Alternatively, clear response when switching endpoints:
onClick={() => { setActiveEndpoint(activeEndpoint === endpoint.id ? null : endpoint.id); setResponse(null); setError(null); }}
274-309: Consider movingCurlExampleCardoutside the component.
CurlExampleCardis defined insideAPIDocsClient, so it's recreated on every render. Since it only depends on props and theonSignIncallback, extracting it as a separate component or memoizing it would avoid unnecessary re-renders.packages/kal-backend/src/lib/db.ts (1)
10-18: Consider validating required MongoDB environment variables.If
MONGODB_USER,MONGODB_PASSWORD, orMONGODB_DATABASEare undefined whenDATABASE_URLis not set, the constructed URI will contain literal"undefined"strings, leading to connection failures with unclear error messages.🔎 Suggested validation
const getDatabaseUri = () => { if (process.env.DATABASE_URL) { return process.env.DATABASE_URL; } const { MONGODB_HOST = "localhost", MONGODB_PORT = "27017", MONGODB_USER, MONGODB_PASSWORD, MONGODB_DATABASE, } = process.env; + + if (!MONGODB_USER || !MONGODB_PASSWORD || !MONGODB_DATABASE) { + throw new Error( + "Missing required MongoDB env vars: MONGODB_USER, MONGODB_PASSWORD, MONGODB_DATABASE. " + + "Alternatively, provide DATABASE_URL." + ); + } return `mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}?authSource=admin`; };packages/kal-frontend/src/app/callback/route.ts (1)
7-12: Consider adding error handling for sign-in failures.If
handleSignInthrows (e.g., invalid state, CSRF mismatch, network error), the user will see an unhandled error page. Consider wrapping in try-catch to redirect to an error page or show a user-friendly message.🔎 Proposed error handling
export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; - await handleSignIn(logtoConfig, searchParams); - // Redirect to login success page, which will then redirect to dashboard - redirect("/login-success"); + try { + await handleSignIn(logtoConfig, searchParams); + // Redirect to login success page, which will then redirect to dashboard + redirect("/login-success"); + } catch (error) { + console.error("Sign-in callback failed:", error); + // Redirect to home or error page with error indication + redirect("/?error=signin_failed"); + } }docker/docker-compose.yml (1)
40-41: Consider the operational impact of hardcoded ports.Changing from variable port mappings (
${LOGTO_PORT:-3301}:3001) to fixed ports (3001:3001) simplifies the configuration but removes deployment flexibility. This means:
- Multiple instances cannot run on the same host without manual port overrides
- Port conflicts will require docker-compose.override.yml or command-line flags
If this setup is only for local development, the simplification is reasonable. For production or multi-environment deployments, consider restoring the environment variable approach.
packages/kal-frontend/src/components/sign-out.tsx (1)
7-16: Consider adding error handling and loading state.The
onSignOuthandler is asynchronous but lacks error handling and loading feedback. Users won't see any indication that sign-out is in progress if the operation is slow or fails.🔎 Suggested enhancement with loading state
'use client'; +import { useState } from 'react'; + type Props = { onSignOut: () => Promise<void>; }; export default function SignOut({ onSignOut }: Props) { + const [isLoading, setIsLoading] = useState(false); + + const handleSignOut = async () => { + setIsLoading(true); + try { + await onSignOut(); + } catch (error) { + console.error('Sign out failed:', error); + // Optionally show error toast/message + } finally { + setIsLoading(false); + } + }; + return ( <button - onClick={() => onSignOut()} + onClick={handleSignOut} + disabled={isLoading} className="sign-out-btn" > - Sign Out + {isLoading ? 'Signing out...' : 'Sign Out'} </button> ); }packages/kal-frontend/src/app/login-success/page.tsx (1)
13-26: Consider a more generic fallback username.The fallback chain
claims?.name || claims?.email || "Developer"is functional, but the final fallback "Developer" might not accurately represent all users. Consider using a more neutral term like "User" or "Member".🔎 Suggested change
<LoginSuccessClient - userName={claims?.name || claims?.email || "Developer"} + userName={claims?.name || claims?.email || "User"} />packages/kal-db/migrations/20241225000003_create_api_keys.js (1)
20-24: Clarify the comment about hashed keys.The comment states "key is hashed" but the index is on the plain
keyfield. If the application stores hashed API keys (which is good security practice), the comment is accurate. If keys are stored in plaintext, consider updating the comment or implementing key hashing.packages/kal-db/scripts/seed.ts (3)
189-201: Consider dropping the oldfoodscollection entirely.The old
foodscollection is cleared but not dropped, which may cause confusion if developers query it expecting data. If it's being deprecated in favor ofnatural_foodsandhalal_foods, consider dropping it.🔎 Proposed change
- // Clear old 'foods' collection (if exists) - await oldFoodsCollection.deleteMany({}); - console.log("🗑️ Cleared old 'foods' collection"); + // Drop old 'foods' collection (if exists) + try { + await db.dropCollection("foods"); + console.log("🗑️ Dropped old 'foods' collection"); + } catch (e) { + console.log("ℹ️ 'foods' collection doesn't exist, skipping drop"); + }
206-214: Index creation may fail on re-runs if index options differ.
createIndexwill throw if an index with the same name but different options exists. Consider using{ background: true }or wrapping in try-catch for idempotent re-runs.🔎 Proposed fix for idempotent index creation
// Create indexes for natural_foods - await naturalCollection.createIndex({ name: "text" }); - await naturalCollection.createIndex({ category: 1 }); + await naturalCollection.dropIndexes().catch(() => {}); + await naturalCollection.createIndex({ name: "text" }); + await naturalCollection.createIndex({ category: 1 }); console.log("✅ Created indexes on 'natural_foods' collection");Alternatively, wrap each
createIndexin a try-catch that ignores "index already exists" errors.
32-154: Large inline data arrays reduce maintainability.Consider extracting the seed data to separate JSON files (e.g.,
data/natural-foods.json,data/halal-foods.json) for easier maintenance and cleaner separation of code and data.packages/kal-frontend/src/app/api-docs/page.tsx (2)
39-43: Security-by-obscurity is ineffective here.The comment claims this prevents viewing data via DevTools, but:
- The API endpoints are publicly documented and discoverable
- The curl commands contain no secrets—just endpoint URLs
- The structure (comment, type) is still sent to the client
If the goal is to gate documentation access, consider redirecting unauthenticated users entirely or removing this logic since it adds complexity without meaningful protection.
50-57: Inline server actions in JSX props may cause issues.Defining server actions inline within props with
'use server'is unconventional. Consider extracting these to named functions at module scope for clarity and to ensure proper serialization.🔎 Suggested refactor
+async function handleSignIn() { + 'use server'; + await signIn(logtoConfig); +} + +async function handleSignOut() { + 'use server'; + await signOut(logtoConfig); +} + export default async function APIDocsPage() { const { isAuthenticated, claims } = await getLogtoContext(logtoConfig); // ... return ( <APIDocsClient isAuthenticated={isAuthenticated} userEmail={claims?.email || claims?.sub} curlExamples={safeCurlExamples} - onSignIn={async () => { - 'use server'; - await signIn(logtoConfig); - }} - onSignOut={async () => { - 'use server'; - await signOut(logtoConfig); - }} + onSignIn={handleSignIn} + onSignOut={handleSignOut} /> ); }packages/kal-frontend/src/app/dashboard/page.tsx (1)
31-33: Consider using Next.jsLinkfor client-side navigation.Using a plain
<a>tag will trigger a full page reload. For better UX and performance in Next.js, use theLinkcomponent.🔎 Suggested change
+import Link from "next/link"; + // In the JSX: - <a href="/" className="back-link"> + <Link href="/" className="back-link"> ← Back to Home - </a> + </Link>packages/kal-db/scripts/seed-safe.ts (2)
195-211: Indexes may not be created if seed is interrupted.If the script fails after
insertManybut beforecreateIndex, subsequent runs will skip the entire block because the collection is non-empty. Consider separating index creation from data seeding, or usingcreateIndexwith{ background: true }idempotently outside the empty-check.🔎 Suggested approach
+ // Always ensure indexes exist (createIndex is idempotent) + await naturalCollection.createIndex({ name: "text" }); + await naturalCollection.createIndex({ category: 1 }); + if (naturalCount > 0) { console.log(`ℹ️ natural_foods collection already has ${naturalCount} documents - skipping`); } else { await naturalCollection.insertMany(naturalFoods); console.log(`✅ Inserted ${naturalFoods.length} items into 'natural_foods' collection`); - - // Create indexes - await naturalCollection.createIndex({ name: "text" }); - await naturalCollection.createIndex({ category: 1 }); - console.log("✅ Created indexes on 'natural_foods' collection"); } + console.log("✅ Ensured indexes on 'natural_foods' collection");
165-180: Limited halal foods dataset.The
halalFoodsarray contains only 13 Ramly products. Consider documenting that this is intentional seed data or expanding with additional brands/items for a more representative dataset.packages/kal-backend/src/routers/api.ts (1)
38-49: Consider extracting food-to-DTO mapping helpers to reduce duplication.The mapping logic for natural foods and halal foods is repeated across multiple endpoints. Extract reusable mapper functions.
🔎 Example helper
const mapNaturalFood = (food: Document) => ({ id: food._id.toString(), name: food.name, calories: food.calories, protein: food.protein ?? 0, carbs: food.carbs ?? 0, fat: food.fat ?? 0, serving: food.serving, category: food.category, }); const mapHalalFood = (food: Document) => ({ ...mapNaturalFood(food), brand: food.brand, halalCertifier: food.halalCertifier, halalCertYear: food.halalCertYear, });Also applies to: 216-228, 273-285, 352-364
packages/kal-backend/src/middleware/rate-limit.ts (1)
43-43: Avoid setting_idto an empty string.Setting
_id: "" as anyis unnecessary—MongoDB will auto-generate the_idif omitted. This also causes TypeScript to lose type safety.- usage = { - _id: "" as any, - userId, + const newUsage = { + userId, // ... rest of fields }; + await collection.insertOne(newUsage);packages/kal-backend/src/routers/halal.ts (1)
31-47: Unbounded query may cause performance issues at scale.The
allprocedure fetches all documents without pagination. Ifhalal_foodsgrows large, this will consume significant memory and increase response times. Consider adding a reasonable limit or deprecating in favor ofallPaginated.🔎 Suggested mitigation
// Get all halal foods (public) - all: publicProcedure.query(async ({ ctx }) => { - const foods = await ctx.db.collection("halal_foods").find({}).toArray(); + all: publicProcedure.query(async ({ ctx }) => { + const foods = await ctx.db + .collection("halal_foods") + .find({}) + .limit(500) // Safeguard against unbounded results + .toArray();packages/kal-frontend/src/app/dashboard/client.tsx (2)
91-97: Add error handling for clipboard API.
navigator.clipboard.writeText()can fail (e.g., non-HTTPS context, permissions denied). Consider wrapping in try-catch with user feedback.🔎 Suggested fix
const handleCopyKey = () => { if (generatedKey) { - navigator.clipboard.writeText(generatedKey); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + navigator.clipboard.writeText(generatedKey) + .then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }) + .catch(() => { + // Fallback or show error message + alert("Failed to copy. Please copy manually."); + }); } };
107-111: Consider replacingconfirm()with a custom modal.The native
confirm()dialog blocks the main thread and has inconsistent styling across browsers. A custom confirmation modal would provide better UX and match the existing modal pattern used for key generation.packages/kal-backend/src/index.ts (1)
109-117: Session cookie missingsecureandsameSiteoptions.For production with credentials-enabled CORS, consider setting
cookie.secure: true(requires HTTPS) andcookie.sameSite: 'lax'or'none'to prevent CSRF and ensure cross-origin requests work properly.🔎 Proposed enhancement
app.use( session({ secret: process.env.SESSION_SECRET || "dev-secret-change-in-production", - cookie: { maxAge: 14 * 24 * 60 * 60 * 1000 }, // 14 days + cookie: { + maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + }, resave: false, saveUninitialized: false, }) );packages/kal-backend/src/middleware/api-key-middleware.ts (2)
54-58: Suspicious type casting for ObjectId lookup.The cast
new ObjectId(keyDoc.userId) as unknown as stringsuggests a type mismatch between theUserinterface (where_idis typed asstring) and MongoDB's actualObjectId. This works at runtime but obscures the real types. Consider aligning theUsertype with MongoDB'sObjectIdor using a wrapper type.🔎 Proposed fix
// Get the user const { ObjectId } = await import("mongodb"); - const user = await db.collection<User>("users").findOne({ - _id: new ObjectId(keyDoc.userId) as unknown as string, - }); + const user = await db.collection<User>("users").findOne({ + _id: new ObjectId(keyDoc.userId), + } as any);Alternatively, update the shared
Usertype to useObjectId | stringfor_id, or create a DB-specific user type.
64-68: Fire-and-forget update is acceptable but consider logging context.The
lastUsedAtupdate is correctly non-blocking. The.catch(console.error)will log errors, but consider adding context (e.g., key ID) for debugging.packages/kal-backend/src/routers/api-keys.ts (2)
46-53: getMe returns fallback values that may mask issues.Returning empty strings and "free" tier when
ctx.useris undefined could hide authentication problems. Since this usesprotectedProcedure, the user should always exist.🔎 Consider throwing if user is missing
getMe: protectedProcedure.query(async ({ ctx }) => { + if (!ctx.user) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "User not found" }); + } return { - _id: ctx.user?._id?.toString() || "", - name: ctx.user?.name || "", - email: ctx.user?.email || "", - tier: ctx.user?.tier || "free", + _id: ctx.user._id.toString(), + name: ctx.user.name || "", + email: ctx.user.email || "", + tier: ctx.user.tier, }; }),
124-146: Use TRPCError for consistency with tRPC error handling.Throwing a plain
Errordoesn't integrate well with tRPC's error handling. UseTRPCErrorwith an appropriate code.🔎 Proposed fix
+import { TRPCError } from "@trpc/server"; + // In the revoke mutation: if (result.matchedCount === 0) { - throw new Error("API key not found or already revoked"); + throw new TRPCError({ + code: "NOT_FOUND", + message: "API key not found or already revoked", + }); }packages/kal-backend/src/lib/context.ts (3)
52-61: Same ObjectId type casting pattern as elsewhere.Consider creating a shared helper for typed ObjectId lookups to avoid this pattern throughout the codebase.
93-101: Redundant condition on line 95.The check
if (headerLogtoId)at line 95 is always true since it's inside theif (headerLogtoId)block at line 86.🔎 Proposed simplification
// If user not found but we have claims in headers (trusted from frontend), create/sync them if (!user) { - // Proceed if we at least have an email or if the ID is sufficient for a basic record - if (headerLogtoId) { - user = await syncUserFromLogto(db, { - sub: headerLogtoId, - email: headerEmail, - name: headerName, - }); - } + user = await syncUserFromLogto(db, { + sub: headerLogtoId, + email: headerEmail, + name: headerName, + }); } else if (headerName && !user.name) {
102-110: Partial state update doesn't reflect all DB changes locally.After updating the user in the database, the local
userobject is manually patched. If additional fields were updated (e.g.,updatedAt), they won't be reflected. Consider re-fetching or ensuring consistency.🔎 Alternative: re-fetch after update
} else if (headerName && !user.name) { - // Update if local name is missing but header has one - await db.collection<User>("users").updateOne( - { _id: user._id }, - { $set: { name: headerName, email: headerEmail || user.email } } - ); - user.name = headerName; - if (headerEmail) user.email = headerEmail; + // Update if local name is missing but header has one + const updatedUser = await db.collection<User>("users").findOneAndUpdate( + { _id: user._id }, + { $set: { name: headerName, email: headerEmail || user.email, updatedAt: new Date() } }, + { returnDocument: "after" } + ); + if (updatedUser) user = updatedUser; }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (56)
.env.example.prettierignore.prettierrcREADME.mddocker/docker-compose.ymldocs/linting.mdeslint.config.mjspackage.jsonpackages/kal-backend/package.jsonpackages/kal-backend/src/index.tspackages/kal-backend/src/lib/context.tspackages/kal-backend/src/lib/db.tspackages/kal-backend/src/lib/logto.tspackages/kal-backend/src/lib/trpc.tspackages/kal-backend/src/middleware/api-key-middleware.tspackages/kal-backend/src/middleware/rate-limit.tspackages/kal-backend/src/routers/api-keys.tspackages/kal-backend/src/routers/api.tspackages/kal-backend/src/routers/food.tspackages/kal-backend/src/routers/halal.tspackages/kal-backend/src/routers/index.tspackages/kal-backend/tsconfig.jsonpackages/kal-db/migrations/20241225000001_add_user_tiers.jspackages/kal-db/migrations/20241225000002_rate_limit_indexes.jspackages/kal-db/migrations/20241225000003_create_api_keys.jspackages/kal-db/package.jsonpackages/kal-db/scripts/seed-safe.tspackages/kal-db/scripts/seed.tspackages/kal-frontend/.env.examplepackages/kal-frontend/package.jsonpackages/kal-frontend/src/app/api-docs/client.tsxpackages/kal-frontend/src/app/api-docs/page.tsxpackages/kal-frontend/src/app/callback/route.tspackages/kal-frontend/src/app/dashboard/client.tsxpackages/kal-frontend/src/app/dashboard/page.tsxpackages/kal-frontend/src/app/globals.csspackages/kal-frontend/src/app/layout.tsxpackages/kal-frontend/src/app/login-success/client.tsxpackages/kal-frontend/src/app/login-success/page.tsxpackages/kal-frontend/src/app/page.tsxpackages/kal-frontend/src/app/search/page.tsxpackages/kal-frontend/src/components/landing/CTA.tsxpackages/kal-frontend/src/components/landing/FAQ.tsxpackages/kal-frontend/src/components/landing/Footer.tsxpackages/kal-frontend/src/components/landing/Navbar.tsxpackages/kal-frontend/src/components/landing/SampleFoods.tsxpackages/kal-frontend/src/components/navbar.tsxpackages/kal-frontend/src/components/sign-in.tsxpackages/kal-frontend/src/components/sign-out.tsxpackages/kal-frontend/src/lib/auth-context.tsxpackages/kal-frontend/src/lib/logto.tspackages/kal-frontend/src/lib/trpc-provider.tsxpackages/kal-frontend/src/lib/trpc.tspackages/kal-shared/package.jsonpackages/kal-shared/src/types/index.tsturbo.json
🧰 Additional context used
🧬 Code graph analysis (12)
packages/kal-backend/src/lib/logto.ts (1)
packages/kal-frontend/src/lib/logto.ts (1)
logtoConfig(3-10)
packages/kal-frontend/src/app/login-success/page.tsx (3)
packages/kal-frontend/src/app/dashboard/page.tsx (1)
metadata(8-11)packages/kal-frontend/src/app/layout.tsx (1)
metadata(10-13)packages/kal-frontend/src/app/login-success/client.tsx (1)
LoginSuccessClient(10-48)
packages/kal-backend/src/routers/index.ts (3)
packages/kal-backend/src/lib/trpc.ts (1)
router(7-7)packages/kal-backend/src/routers/halal.ts (1)
halalRouter(5-135)packages/kal-backend/src/routers/api-keys.ts (1)
apiKeysRouter(42-179)
packages/kal-frontend/src/lib/auth-context.tsx (1)
packages/kal-backend/src/lib/context.ts (1)
createContext(63-125)
packages/kal-frontend/src/app/dashboard/client.tsx (3)
packages/kal-shared/src/types/index.ts (1)
ApiKeyExpiration(53-53)packages/kal-frontend/src/lib/auth-context.tsx (2)
AuthUpdater(50-68)useAuth(19-21)packages/kal-frontend/src/lib/trpc.ts (1)
trpc(5-5)
packages/kal-backend/src/middleware/api-key-middleware.ts (4)
packages/kal-shared/src/types/index.ts (2)
User(6-14)ApiKey(55-67)packages/kal-backend/src/lib/db.ts (1)
getDB(35-38)packages/kal-db/migrations/20241225000001_add_user_tiers.js (1)
result(9-12)packages/kal-backend/src/middleware/rate-limit.ts (2)
checkRateLimit(18-110)getRateLimitHeaders(125-150)
packages/kal-backend/src/routers/api-keys.ts (4)
packages/kal-shared/src/types/index.ts (4)
ApiKeyExpiration(53-53)ApiKey(55-67)ApiKeyPublic(70-79)RateLimitUsage(30-38)packages/kal-backend/src/lib/trpc.ts (2)
router(7-7)protectedProcedure(26-26)packages/kal-backend/src/middleware/api-key-middleware.ts (2)
generateApiKey(21-28)hashApiKey(14-16)packages/kal-db/migrations/20241225000001_add_user_tiers.js (1)
result(9-12)
packages/kal-frontend/src/lib/logto.ts (1)
packages/kal-backend/src/lib/logto.ts (1)
logtoConfig(4-9)
packages/kal-frontend/src/app/search/page.tsx (1)
packages/kal-frontend/src/lib/trpc.ts (1)
trpc(5-5)
packages/kal-backend/src/routers/api.ts (2)
packages/kal-backend/src/middleware/api-key-middleware.ts (1)
validateApiKeyMiddleware(76-146)packages/kal-backend/src/lib/db.ts (1)
getDB(35-38)
packages/kal-frontend/src/components/navbar.tsx (4)
packages/kal-frontend/src/components/landing/Navbar.tsx (1)
Navbar(16-87)packages/kal-frontend/src/lib/logto.ts (1)
logtoConfig(3-10)packages/kal-frontend/src/components/sign-out.tsx (1)
SignOut(7-16)packages/kal-frontend/src/components/sign-in.tsx (1)
SignIn(7-16)
packages/kal-frontend/src/lib/trpc-provider.tsx (2)
packages/kal-frontend/src/lib/auth-context.tsx (1)
useAuth(19-21)packages/kal-frontend/src/lib/trpc.ts (1)
trpc(5-5)
🪛 Biome (2.1.2)
packages/kal-frontend/src/app/api-docs/client.tsx
[error] 422-423: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".
Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.
(lint/security/noBlankTarget)
🪛 dotenv-linter (4.0.0)
packages/kal-frontend/.env.example
[warning] 6-6: [UnorderedKey] The NEXT_PUBLIC_LOGTO_APP_ID key should go before the NEXT_PUBLIC_LOGTO_ENDPOINT key
(UnorderedKey)
[warning] 7-7: [UnorderedKey] The NEXT_PUBLIC_APP_URL key should go before the NEXT_PUBLIC_LOGTO_APP_ID key
(UnorderedKey)
.env.example
[warning] 14-14: [UnorderedKey] The LOGTO_ADMIN_ENDPOINT key should go before the LOGTO_ENDPOINT key
(UnorderedKey)
[warning] 30-30: [UnorderedKey] The BACKEND_BASE_URL key should go before the BACKEND_PORT key
(UnorderedKey)
[warning] 31-31: [UnorderedKey] The FRONTEND_PORT key should go before the NEXT_PUBLIC_API_URL key
(UnorderedKey)
[warning] 32-32: [UnorderedKey] The FRONTEND_URL key should go before the NEXT_PUBLIC_API_URL key
(UnorderedKey)
🔇 Additional comments (63)
packages/kal-frontend/src/components/landing/FAQ.tsx (1)
3-6: Good import organization.The blank line at line 4 properly separates React imports from local component imports, following standard conventions. This aligns with ESLint/Prettier formatting rules mentioned in the PR and improves code readability.
turbo.json (1)
15-18: LGTM!The
lint:fixtask is correctly configured withcache: falsesince it modifies source files, and the^lint:fixdependency ensures workspace dependencies are processed first.packages/kal-frontend/src/app/search/page.tsx (5)
19-26: LGTM!The
HalalFoodinterface correctly extendsFoodwith halal-specific optional fields, and theTabTypeunion type provides good type safety for tab switching.
62-88: LGTM!The natural foods queries are properly gated by
activeTab === "natural", preventing unnecessary network requests when the halal tab is active. The infinite query configuration withgetNextPageParamandinitialCursorlooks correct.
90-116: LGTM!The halal foods queries mirror the natural foods query structure consistently, with proper
enabledguards based onactiveTab.
118-146: LGTM!The Intersection Observer is correctly configured with appropriate guards for the active tab and search mode. The dependency array is comprehensive, ensuring the observer is recreated when relevant state changes.
148-171: LGTM!The data derivation logic correctly selects the appropriate data source based on the active tab and search mode. The ternary expressions are clear and the types align properly.
packages/kal-frontend/src/components/landing/CTA.tsx (1)
2-2: LGTM! Import ordering aligns with new Prettier config.The import reordering is a formatting change that aligns with the new ESLint/Prettier configuration introduced in this PR.
packages/kal-frontend/src/components/landing/Footer.tsx (1)
2-2: LGTM! Formatting aligns with new Prettier standards.The blank line addition follows standard import grouping conventions from the new Prettier configuration.
packages/kal-frontend/src/components/landing/SampleFoods.tsx (1)
1-1: LGTM! Import ordering is now consistent.The Button import repositioning aligns with the standardized import ordering from the new ESLint/Prettier setup.
packages/kal-backend/tsconfig.json (1)
9-9: LGTM! Declaration maps improve debugging.Enabling
declarationMapgenerates.d.ts.mapfiles that enhance IDE navigation and debugging support for TypeScript declarations across the monorepo..prettierignore (1)
1-20: LGTM! Standard ignore patterns for Prettier.The ignore patterns appropriately exclude dependencies, build artifacts, lock files, and generated code from formatting. This is standard practice and well-suited for the monorepo structure.
packages/kal-shared/package.json (1)
10-11: LGTM! Lint scripts align with monorepo standards.The addition of
lintandlint:fixscripts integrates kal-shared with the broader linting infrastructure established in this PR..prettierrc (1)
1-12: LGTM! Balanced and standard Prettier configuration.The configuration uses sensible defaults that promote consistency across the monorepo. Settings like
printWidth: 80,trailingComma: "es5", andendOfLine: "lf"are well-suited for team collaboration and cross-platform development.README.md (1)
215-221: LGTM! Clear and actionable contributing workflow.The updated workflow emphasizes linting and provides clear, sequential steps for contributors. The reference to
docs/linting.mdoffers additional guidance without cluttering the main README.packages/kal-db/package.json (1)
13-14: LGTM!The lint scripts are properly configured and align with the monorepo's linting strategy.
docs/linting.md (1)
1-81: LGTM!Comprehensive linting documentation that clearly explains the tooling setup, workflow, and VS Code integration. The pre-push checklist is particularly helpful.
packages/kal-shared/src/types/index.ts (4)
1-14: LGTM!The user tier system and integration into the User interface is well-designed.
16-38: LGTM!The rate limiting types and configuration are well-structured. The hardcoded limits in
RATE_LIMITSestablish clear tier boundaries: free (100/day, 10/min), tier_1 (250/day, 30/min), and tier_2 (700/day, 50/min).
40-48: LGTM!The
LogtoUserInfointerface correctly models the JWT claims from Logto's authentication response.
50-79: LGTM!The API key types follow security best practices by storing hashed keys and only exposing the prefix for display. The separation between
ApiKeyandApiKeyPublicprevents accidental exposure of sensitive fields.packages/kal-frontend/package.json (2)
9-11: LGTM!The lint scripts are properly configured and align with the monorepo's linting strategy.
14-21: LGTM!The addition of
@logto/nextfor authentication andkal-sharedfor shared types are appropriate dependencies for the frontend.packages/kal-frontend/src/app/globals.css (1)
45-665: LGTM!Comprehensive styling system with good practices:
- Consistent use of CSS custom properties for theming
- Accessibility features (focus-visible, selection colors)
- Responsive design with mobile breakpoints
- Clear component-scoped class naming
- Smooth transitions and animations
package.json (1)
9-11: LGTM!The lint and format scripts follow standard patterns and integrate well with Turbo's task orchestration.
eslint.config.mjs (3)
1-32: Well-structured ESLint flat config for monorepo.The configuration is well-organized with clear separation of concerns for backend/shared TypeScript, React/Next.js frontend, and plain JavaScript files. The ignore patterns and rule selections follow best practices.
56-94: TypeScript rules are appropriately configured.Good use of ignore patterns for unused variables (
^_prefix) and warning-level severity forno-explicit-anyandconsistent-type-imports. The import ordering rules will help maintain consistent code organization.
148-154: React hooks rules correctly configured.
rules-of-hooksas error andexhaustive-depsas warn is the recommended configuration for catching hook violations while allowing flexibility for intentional dependency omissions.packages/kal-frontend/src/app/api-docs/client.tsx (2)
10-16: getApiUrl correctly handles client-side environment.The function properly checks for browser environment before accessing
process.env.NEXT_PUBLIC_API_URLand falls back gracefully to empty string for relative paths.
315-571: Well-structured API documentation UI.The component provides a clean, organized presentation of API endpoints with interactive "Try it" functionality. The authentication-gated cURL examples and responsive design are well implemented.
packages/kal-frontend/src/components/landing/Navbar.tsx (1)
3-5: Import reordering aligns with ESLint config.The blank line between external imports (
next/link,react) and internal imports (@/components/...) follows theimport/orderrule with"newlines-between": "always"configured in the ESLint setup.packages/kal-backend/src/lib/trpc.ts (2)
1-3: Correct use of type-only import forContext.Since
Contextis only used as a type parameter forinitTRPC.context<Context>(), theimport typesyntax is appropriate and aligns with the@typescript-eslint/consistent-type-importsrule configured in the ESLint setup.
10-24: Authentication middleware correctly implemented.The
isAuthenticatedmiddleware properly guards protected procedures by checking foruserIdpresence and throws an appropriateUNAUTHORIZEDerror. The context spreading pattern preserves existing context while ensuring type narrowing foruserId.packages/kal-frontend/src/lib/trpc.ts (1)
1-5: Import reordering and tRPC setup looks correct.The type import for
AppRouteris properly separated. The explicit type annotation on thetrpcexport ensures consistent typing across the frontend.packages/kal-frontend/src/app/layout.tsx (2)
3-6: Import organization follows ESLint rules.The blank line separating CSS import from component imports aligns with the configured
import/orderrule.
23-25: AuthProvider correctly handleslogtoId={null}.The
AuthProvidercomponent acceptslogtoId: string | null(packages/kal-frontend/src/lib/auth-context.tsx, line 28) and properly initializes state with the provided null value. This pattern is appropriate for server-rendered layouts where authentication state is unknown at render time.packages/kal-backend/src/lib/db.ts (1)
1-2: Correct separation of type and value imports.Splitting
Dbas a type-only import andMongoClientas a runtime import follows TypeScript best practices and theconsistent-type-importsrule.packages/kal-frontend/.env.example (1)
4-11: LGTM! Environment variable structure is well-organized.The separation between client-side (
NEXT_PUBLIC_*) and server-side secrets is correct. The port changes from 3301/3302 to 3001/3002 align with the docker-compose.yml updates.packages/kal-backend/src/routers/index.ts (1)
1-15: LGTM! Clean router composition.The addition of
halalRouterandapiKeysRouterfollows the existing pattern and properly extends the backend API surface. The exportedAppRoutertype will automatically reflect these new routes for type-safe client usage.packages/kal-frontend/src/app/page.tsx (1)
1-33: LGTM! Landing page composition enhanced.The import reorganization and addition of new landing components (Features, FAQ, FinalCTA, Footer) maintain a logical flow for the landing page experience.
packages/kal-db/migrations/20241225000002_rate_limit_indexes.js (2)
6-22: LGTM! Well-designed indexes for rate limiting.The compound unique index on
(userId, date)ensures efficient lookups and prevents duplicate records per user per day. The TTL index with 7-day expiry provides automatic cleanup of stale data, which is appropriate for rate-limiting use cases.
24-40: LGTM! Resilient rollback handling.The
down()migration properly handles cases where indexes might not exist, logging informational messages rather than failing. This makes the migration safe to run multiple times.packages/kal-backend/package.json (1)
17-17: No action required—the specified package versions are verified as free from known public vulnerabilities and are current:
@logto/express@3.0.12: Latest version, no CVEs recordedcookie-parser@1.4.7: No known vulnerabilitiesexpress-session@1.18.2: No CVEs affecting this versionpackages/kal-backend/src/lib/logto.ts (1)
1-20: LGTM!Clean Logto configuration module with appropriate environment variable handling. The validation function provides graceful degradation when credentials are missing, and the warning message clearly indicates which variables need to be set.
packages/kal-db/migrations/20241225000001_add_user_tiers.js (2)
6-23: LGTM!The migration correctly handles idempotent collection creation and applies a sensible default tier. The
updateManywith$exists: falsefilter ensures only users without a tier get updated, which is safe for re-runs.
25-41: Down migration is well-structured.Good error handling for the collection drop. Since migration 20241225000002 creates indexes on
rate_limit_usage, ensure migrations are rolled back in reverse order (20241225000002 down first, then this one) to avoid issues.packages/kal-frontend/src/components/navbar.tsx (1)
7-37: LGTM!Clean implementation of server-driven authentication in the Navbar. The inline
'use server'directives for signIn/signOut actions are valid for Next.js 15 server actions, and the optional chaining on claims handles cases where user info might be incomplete.packages/kal-frontend/src/lib/trpc-provider.tsx (1)
17-49: LGTM!The ref pattern is the correct approach here. Since
trpcClientis created once (viauseStateinitializer), theheaders()callback captures the ref object itself, not its current value. UpdatingauthRef.currenton each render ensures the callback always accesses the latest auth state when making requests.The
credentials: "include"setting is appropriate for cookie-based authentication with cross-origin requests.packages/kal-frontend/src/app/login-success/client.tsx (1)
10-47: LGTM!Clean countdown implementation with proper effect separation and cleanup. The interval is correctly cleared on unmount, preventing memory leaks.
packages/kal-db/migrations/20241225000003_create_api_keys.js (1)
10-40: LGTM!Well-designed indexes for the API keys use cases:
key_uniquefor fast authentication lookupsuser_keyscompound index optimizes listing a user's active keys by creation dateexpirationindex supports cleanup queriespackages/kal-backend/src/routers/food.ts (1)
1-92: LGTM! Collection rename is consistent.The migration from
foodstonatural_foodscollection is applied consistently across all public endpoints (search, all, allPaginated, categories). The field mappings remain intact.packages/kal-frontend/src/app/dashboard/client.tsx (1)
28-50: LGTM! Clean auth synchronization pattern.The
AuthUpdater+DashboardContentWrapperpattern ensures the auth context is synchronized before rendering protected content. This prevents stale state issues with server-passed props.packages/kal-frontend/src/app/dashboard/page.tsx (1)
36-40: The use of optional chaining (claims?.sub) is intentional and safe. Althoughclaimsis guaranteed to be defined after theisAuthenticatedcheck, the optional chaining protects againstsubpotentially missing from the claims object. All downstream components—AuthUpdaterandDashboardContentWrapper—are designed to gracefully handleundefinedlogtoId, so no guard or assertion is needed. This is defensive programming and follows the existing pattern in the codebase.packages/kal-backend/src/index.ts (2)
95-107: CORS and middleware setup looks good.The CORS configuration correctly filters out undefined origins and enables credentials for cookie-based auth. Middleware ordering (cors → json → cookieParser) is appropriate.
119-123: Conditional Logto route registration is well-handled.Gracefully degrades when Logto is not configured, with appropriate logging.
packages/kal-backend/src/middleware/api-key-middleware.ts (2)
14-16: Hashing implementation is correct.SHA-256 is appropriate for API key storage/lookup.
76-145: Middleware uses Promise-based flow correctly.The
.then()/.catch()pattern handles async validation properly. The rate limit headers are set before checking limits, and the user is attached to the request for downstream use.packages/kal-backend/src/routers/api-keys.ts (3)
14-24: Expiration calculation is correct.The switch handles all
ApiKeyExpirationvariants. Using millisecond math for 30-day month is reasonable for this use case.
82-119: API key generation implementation is solid.The flow correctly generates a key, hashes for storage, and returns the plain key only once. The key prefix extraction and document structure align with the shared types.
1-9: No issues found. The import ofRateLimitUsagefromkal-sharedat line 1 is correct.RateLimitUsageis defined only inpackages/kal-shared/src/types/index.tsand is not duplicated locally in this file.Likely an incorrect or invalid review comment.
packages/kal-frontend/src/lib/auth-context.tsx (2)
36-41: setAuth callback is correctly memoized.Using
useCallbackwith an empty dependency array ensures referential stability, preventing unnecessary re-renders in consumers.
23-48: AuthProvider structure is clean and follows React patterns.The provider correctly initializes state from props and exposes a stable setter.
packages/kal-backend/src/lib/context.ts (1)
17-47: syncUserFromLogto implementation is solid.The upsert pattern with
$setand$setOnInsertcorrectly handles both create and update cases. Setting default tier to "free" aligns with the migration.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
.env.example (1)
13-14: Ordering: Semantic grouping vs. alphabetical convention.dotenv-linter flags suggest alphabetical ordering of keys (e.g.,
LOGTO_ADMIN_ENDPOINTbeforeLOGTO_ENDPOINT). However, your current semantic grouping—organized by service (MongoDB, Logto, App)—is more intuitive for developers working with the codebase. If strict linter compliance is a project requirement, consider reordering; otherwise, the current approach is reasonable.Also applies to: 30-32
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
.env.example
🧰 Additional context used
🪛 dotenv-linter (4.0.0)
.env.example
[warning] 14-14: [UnorderedKey] The LOGTO_ADMIN_ENDPOINT key should go before the LOGTO_ENDPOINT key
(UnorderedKey)
[warning] 30-30: [UnorderedKey] The BACKEND_BASE_URL key should go before the BACKEND_PORT key
(UnorderedKey)
[warning] 31-31: [UnorderedKey] The FRONTEND_PORT key should go before the NEXT_PUBLIC_API_URL key
(UnorderedKey)
[warning] 32-32: [UnorderedKey] The FRONTEND_URL key should go before the NEXT_PUBLIC_API_URL key
(UnorderedKey)
🔇 Additional comments (1)
.env.example (1)
17-18: ✅ Security fix: Credentials properly replaced with empty placeholders.The critical issues flagged in previous reviews have been addressed—
LOGTO_APP_ID,LOGTO_APP_SECRET, andSESSION_SECRETare now empty placeholders rather than containing actual secrets. This correctly prevents real credentials from being committed to version control.Consider adding brief inline comments to guide developers on how to populate these fields (e.g., how to generate a secure
SESSION_SECRETusingopenssl rand -base64 32or where to find Logto credentials in the admin console).Also applies to: 35-35
Summary by CodeRabbit
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.