Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. WalkthroughAdds admin UI and APIs for team management and waitlist control. Introduces Admin layout and pages: SES configurations update, new Teams and Waitlist pages (cloud-gated) using tRPC mutations. Adds waitlist user-facing page and form with Zod schema and tRPC mutation with Redis rate limiting and founder email notification (FOUNDER_EMAIL env). Extends API root with waitlist router; admin router gains find/update for users’ waitlist and teams’ settings. Refactors trpc auth: adds authedProcedure and updates protectedProcedure to gate waitlisted users. Updates next-auth provider to render WaitListForm for waitlisted users. Minor docs rule added in AGENTS.md. Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/server/auth.ts (1)
84-86: Do not use Math.random() for verification tokens.This is weak and predictable. Use crypto or NextAuth default.
Apply one of the fixes:
- async generateVerificationToken() { - return Math.random().toString(36).substring(2, 7).toLowerCase(); - },or
+ async generateVerificationToken() { + const { randomBytes } = await import("crypto"); + return randomBytes(32).toString("hex"); + },
🧹 Nitpick comments (14)
AGENTS.md (1)
32-35: Fix wording and capitalization in new rule.Use “tRPC” and correct grammar.
Apply this diff:
-## Rules - -- Prefer to use trpc alway unless asked otherwise +## Rules + +- Prefer to use tRPC by default unless asked otherwise.apps/web/src/env.js (1)
53-53: Validate FOUNDER_EMAIL as an email address.Type-level guard reduces misconfig at deploy time.
- FOUNDER_EMAIL: z.string().optional(), + FOUNDER_EMAIL: z.string().email().optional(),apps/web/src/app/wait-list/schema.ts (1)
8-24: Tighten domain validation.Add a basic FQDN check and normalize casing.
export const waitlistSubmissionSchema = z.object({ domain: z .string({ required_error: "Domain is required" }) .trim() + .transform((v) => v.toLowerCase()) .min(1, "Domain is required") - .max(255, "Domain must be 255 characters or fewer"), + .max(255, "Domain must be 255 characters or fewer") + .regex( + /^(?=.{1,255}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})+$/, + "Enter a valid domain (e.g., example.com)" + ), emailTypes: z .array(z.enum(WAITLIST_EMAIL_TYPES)) .min(1, "Select at least one email type"),apps/web/src/server/api/trpc.ts (1)
119-125: Use FORBIDDEN for waitlisted users.This is an authorization failure, not authentication; improves client handling.
- if (ctx.session.user.isWaitlisted) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } + if (ctx.session.user.isWaitlisted) { + throw new TRPCError({ code: "FORBIDDEN", message: "WAITLISTED" }); + }apps/web/src/app/wait-list/waitlist-form.tsx (1)
32-35: Deduplicate EMAIL_TYPE_LABEL across client and server.Same mapping exists in apps/web/src/server/api/routers/waitlist.ts. Centralize in schema to prevent drift.
Add to schema.ts:
+export const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = { + transactional: "Transactional", + marketing: "Marketing", +};Then update this file:
-import { - WAITLIST_EMAIL_TYPES, - waitlistSubmissionSchema, - type WaitlistSubmissionInput, -} from "./schema"; +import { + WAITLIST_EMAIL_TYPES, + waitlistSubmissionSchema, + type WaitlistSubmissionInput, + EMAIL_TYPE_LABEL, +} from "./schema"; - -const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = { - transactional: "Transactional", - marketing: "Marketing", -};apps/web/src/server/api/routers/waitlist.ts (2)
16-19: Move EMAIL_TYPE_LABEL to shared schema to avoid duplication.Use a single source of truth alongside WAITLIST_EMAIL_TYPES.
Example change here (after exporting from schema):
-import { - WAITLIST_EMAIL_TYPES, - waitlistSubmissionSchema, -} from "~/app/wait-list/schema"; +import { + WAITLIST_EMAIL_TYPES, + waitlistSubmissionSchema, + EMAIL_TYPE_LABEL, +} from "~/app/wait-list/schema"; - -const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> = { - transactional: "Transactional", - marketing: "Marketing", -};
61-62: Clarify rate‑limit error message.Consider indicating the window (e.g., “Try again in ~6 hours”) for better UX.
- message: "You have reached the waitlist request limit. Please try later.", + message: "You have reached the waitlist request limit. Please try again in a few hours.",apps/web/src/server/api/routers/admin.ts (2)
110-126: Use query for read‑only RPC.findUserByEmail performs a read; make it a .query for clearer intent and caching semantics.
- findUserByEmail: adminProcedure + findUserByEmail: adminProcedure .input( z.object({ email: z .string() .email() .transform((value) => value.toLowerCase()), }), ) - .mutation(async ({ input }) => { + .query(async ({ input }) => { const user = await db.user.findUnique({ where: { email: input.email }, select: waitlistUserSelection, }); return user ?? null; }),
145-198: Use query for read‑only RPC.findTeam is read‑only and should be a .query for consistency.
- findTeam: adminProcedure + findTeam: adminProcedure .input( z.object({ query: z .string({ required_error: "Search query is required" }) .trim() .min(1, "Search query is required"), }), ) - .mutation(async ({ input }) => { + .query(async ({ input }) => { const query = input.query.trim(); … return team ?? null; }),apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
112-118: Redundant controlled prop.
{...field}already supplies value/onChange. Explicitvalue={field.value}is redundant and risks drift.- <Input + <Input placeholder="user@example.com" autoComplete="off" - {...field} - value={field.value} + {...field} />
44-49: Small UX: disable submit until form is valid and reflect busy state.Prevents useless requests and improves a11y.
const form = useForm<SearchInput>({ - resolver: zodResolver(searchSchema), + resolver: zodResolver(searchSchema), + mode: "onChange", defaultValues: { email: "", }, });- <Button type="submit" disabled={findUser.isPending}> + <Button + type="submit" + disabled={!form.formState.isValid || findUser.isPending} + aria-busy={findUser.isPending} + >Also applies to: 123-131
apps/web/src/app/(dashboard)/admin/teams/page.tsx (3)
253-263: Numeric input jank and mobile keypad.Typing an empty value turns into 1/0 immediately; also, mobile numeric keypad is not hinted.
- Allow transient empty input and coerce on blur; hint numeric keypad.
- <Input + <Input type="number" min={1} max={10000} + inputMode="numeric" + pattern="[0-9]*" {...field} - value={Number.isNaN(field.value) ? 1 : field.value} - onChange={(event) => - field.onChange(Number(event.target.value)) - } + value={Number.isNaN(field.value) ? "" : field.value} + onChange={(e) => { + const v = e.target.value; + field.onChange(v === "" ? Number.NaN : Number(v)); + }} + onBlur={() => { + if (Number.isNaN(field.value)) field.onChange(1); + }} disabled={updateTeam.isPending} />Apply the same pattern to dailyEmailLimit with fallback 0 and min={0}.
Also applies to: 276-286
339-349: A11y: reflect busy state on submit.Add aria-busy to communicate progress to AT.
- <Button type="submit" disabled={updateTeam.isPending}> + <Button + type="submit" + disabled={updateTeam.isPending} + aria-busy={updateTeam.isPending} + >
167-171: Minor UX duplication.You show both a toast ("No team found") and an inline empty-state message. Consider one.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
AGENTS.md(1 hunks)apps/web/src/app/(dashboard)/admin/layout.tsx(1 hunks)apps/web/src/app/(dashboard)/admin/page.tsx(1 hunks)apps/web/src/app/(dashboard)/admin/teams/page.tsx(1 hunks)apps/web/src/app/(dashboard)/admin/waitlist/page.tsx(1 hunks)apps/web/src/app/wait-list/page.tsx(1 hunks)apps/web/src/app/wait-list/schema.ts(1 hunks)apps/web/src/app/wait-list/waitlist-form.tsx(1 hunks)apps/web/src/env.js(2 hunks)apps/web/src/providers/next-auth.tsx(2 hunks)apps/web/src/server/api/root.ts(2 hunks)apps/web/src/server/api/routers/admin.ts(2 hunks)apps/web/src/server/api/routers/waitlist.ts(1 hunks)apps/web/src/server/api/trpc.ts(1 hunks)apps/web/src/server/auth.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Format code with Prettier 3 via pnpm format for TypeScript and Markdown files
Files:
AGENTS.mdapps/web/src/app/wait-list/schema.tsapps/web/src/app/(dashboard)/admin/page.tsxapps/web/src/app/(dashboard)/admin/layout.tsxapps/web/src/server/auth.tsapps/web/src/providers/next-auth.tsxapps/web/src/server/api/routers/admin.tsapps/web/src/app/wait-list/page.tsxapps/web/src/app/wait-list/waitlist-form.tsxapps/web/src/app/(dashboard)/admin/waitlist/page.tsxapps/web/src/app/(dashboard)/admin/teams/page.tsxapps/web/src/server/api/root.tsapps/web/src/server/api/trpc.tsapps/web/src/server/api/routers/waitlist.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports, and ensure proper naming of key components.
Files:
apps/web/src/app/wait-list/schema.tsapps/web/src/app/(dashboard)/admin/page.tsxapps/web/src/app/(dashboard)/admin/layout.tsxapps/web/src/server/auth.tsapps/web/src/providers/next-auth.tsxapps/web/src/server/api/routers/admin.tsapps/web/src/app/wait-list/page.tsxapps/web/src/env.jsapps/web/src/app/wait-list/waitlist-form.tsxapps/web/src/app/(dashboard)/admin/waitlist/page.tsxapps/web/src/app/(dashboard)/admin/teams/page.tsxapps/web/src/server/api/root.tsapps/web/src/server/api/trpc.tsapps/web/src/server/api/routers/waitlist.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Use 2-space indentation in TypeScript code (enforced by Prettier)
Use semicolons in TypeScript code (enforced by Prettier)
Do not use dynamic imports
Files:
apps/web/src/app/wait-list/schema.tsapps/web/src/app/(dashboard)/admin/page.tsxapps/web/src/app/(dashboard)/admin/layout.tsxapps/web/src/server/auth.tsapps/web/src/providers/next-auth.tsxapps/web/src/server/api/routers/admin.tsapps/web/src/app/wait-list/page.tsxapps/web/src/app/wait-list/waitlist-form.tsxapps/web/src/app/(dashboard)/admin/waitlist/page.tsxapps/web/src/app/(dashboard)/admin/teams/page.tsxapps/web/src/server/api/root.tsapps/web/src/server/api/trpc.tsapps/web/src/server/api/routers/waitlist.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
In apps/web, use the
/ alias for src imports (e.g., import { x } from "/utils/x")
Files:
apps/web/src/app/wait-list/schema.tsapps/web/src/app/(dashboard)/admin/page.tsxapps/web/src/app/(dashboard)/admin/layout.tsxapps/web/src/server/auth.tsapps/web/src/providers/next-auth.tsxapps/web/src/server/api/routers/admin.tsapps/web/src/app/wait-list/page.tsxapps/web/src/app/wait-list/waitlist-form.tsxapps/web/src/app/(dashboard)/admin/waitlist/page.tsxapps/web/src/app/(dashboard)/admin/teams/page.tsxapps/web/src/server/api/root.tsapps/web/src/server/api/trpc.tsapps/web/src/server/api/routers/waitlist.ts
**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Name React component files in PascalCase (e.g., AppSideBar.tsx)
Files:
apps/web/src/app/(dashboard)/admin/page.tsxapps/web/src/app/(dashboard)/admin/layout.tsxapps/web/src/providers/next-auth.tsxapps/web/src/app/wait-list/page.tsxapps/web/src/app/wait-list/waitlist-form.tsxapps/web/src/app/(dashboard)/admin/waitlist/page.tsxapps/web/src/app/(dashboard)/admin/teams/page.tsx
🧠 Learnings (3)
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to apps/web/**/*.{ts,tsx} : In apps/web, use the ~/ alias for src imports (e.g., import { x } from "~/utils/x")
Applied to files:
AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{ts,tsx} : Do not use dynamic imports
Applied to files:
AGENTS.md
📚 Learning: 2025-09-08T08:10:01.547Z
Learnt from: CR
PR: usesend/useSend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-08T08:10:01.547Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Do not add tests unless explicitly required
Applied to files:
AGENTS.md
🧬 Code graph analysis (11)
apps/web/src/app/(dashboard)/admin/page.tsx (2)
apps/web/src/app/(dashboard)/admin/add-ses-configuration.tsx (1)
AddSesConfiguration(16-40)apps/web/src/app/(dashboard)/admin/ses-configurations.tsx (1)
SesConfigurations(17-84)
apps/web/src/app/(dashboard)/admin/layout.tsx (2)
apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx (1)
SettingsNavButton(7-39)apps/web/src/utils/common.ts (1)
isCloud(3-5)
apps/web/src/server/auth.ts (1)
apps/web/src/env.js (2)
env(5-134)env(5-134)
apps/web/src/providers/next-auth.tsx (1)
apps/web/src/app/wait-list/waitlist-form.tsx (1)
WaitListForm(37-201)
apps/web/src/server/api/routers/admin.ts (2)
apps/web/src/server/api/trpc.ts (1)
adminProcedure(267-272)apps/web/src/server/db.ts (1)
db(20-20)
apps/web/src/app/wait-list/page.tsx (2)
apps/web/src/server/auth.ts (1)
getServerAuthSession(153-153)apps/web/src/app/wait-list/waitlist-form.tsx (1)
WaitListForm(37-201)
apps/web/src/app/wait-list/waitlist-form.tsx (2)
apps/web/src/app/wait-list/schema.ts (3)
WAITLIST_EMAIL_TYPES(3-6)WaitlistSubmissionInput(24-24)waitlistSubmissionSchema(8-22)packages/ui/src/spinner.tsx (1)
Spinner(4-51)
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
apps/web/src/server/api/root.ts (1)
AppRouter(40-40)apps/web/src/utils/common.ts (1)
isCloud(3-5)
apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
apps/web/src/server/api/root.ts (1)
AppRouter(40-40)apps/web/src/server/service/team-service.ts (1)
updateTeam(104-111)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/waitlist.ts (1)
waitlistRouter(30-107)
apps/web/src/server/api/routers/waitlist.ts (6)
apps/web/src/app/wait-list/schema.ts (2)
WAITLIST_EMAIL_TYPES(3-6)waitlistSubmissionSchema(8-22)apps/web/src/server/api/trpc.ts (2)
createTRPCRouter(82-82)authedProcedure(99-109)apps/web/src/env.js (2)
env(5-134)env(5-134)apps/web/src/server/logger/log.ts (1)
logger(31-63)apps/web/src/server/redis.ts (1)
getRedis(6-13)apps/web/src/server/mailer.ts (1)
sendMail(72-135)
🔇 Additional comments (14)
apps/web/src/server/auth.ts (2)
132-141: Waitlist gating logic change looks good.Behavior aligns with protectedProcedure waitlist checks.
Please verify onboarding flows for:
- Cloud + no invite → user.isWaitlisted = true; UI shows waitlist.
- Self-hosted (NEXT_PUBLIC_IS_CLOUD=false) → not waitlisted.
91-93: Typo in error message.“atleast” → “at least”.
[raise_nitpick_refactor]- throw new Error("No auth providers found, need atleast one"); + throw new Error("No auth providers found, need at least one");apps/web/src/server/api/trpc.ts (1)
99-109: Auth middleware LGTM.Session presence check and context shaping look fine.
apps/web/src/server/api/root.ts (1)
15-16: Router wiring LGTM.waitlistRouter import and exposure are correct.
Also applies to: 36-36
apps/web/src/app/(dashboard)/admin/page.tsx (1)
6-14: Admin SES page refactor LGTM.Header/actions layout and component split are clean.
apps/web/src/app/wait-list/page.tsx (1)
3-15: Waitlist page gating and UI LGTM.Server-side redirect and form embedding are correct.
Also applies to: 17-33
apps/web/src/env.js (1)
107-107: FOUNDER_EMAIL is server-only — no client references found.
Defined at apps/web/src/env.js:107 and only used in apps/web/src/server/api/routers/waitlist.ts:40; no client components reference env.FOUNDER_EMAIL.apps/web/src/app/(dashboard)/admin/layout.tsx (2)
11-29: LGTM: Admin layout and nav structure look good.
21-25: No change needed — isCloud already returns a boolean.apps/web/src/env.js normalizes NEXT_PUBLIC_IS_CLOUD with .transform((str) => str === "true"), so env.NEXT_PUBLIC_IS_CLOUD is a boolean and apps/web/src/utils/common.ts returning it is correct.
apps/web/src/providers/next-auth.tsx (1)
41-56: Nice UX for waitlisted users; clear CTA and consistent styling.apps/web/src/app/(dashboard)/admin/teams/page.tsx (2)
30-33: Good: Strong typing via AppRouter + inferRouterOutputs.Nice use of TRPC types to derive TeamAdmin; avoids drift.
Also applies to: 43-45
85-99: Mutation handling and form sync look correct.Search/update flows, toasts, and reset-on-success are coherent.
Also applies to: 100-114
apps/web/src/app/(dashboard)/admin/waitlist/page.tsx (2)
160-166: Type safety: createdAt conversion.new Date(userResult.createdAt)safely handles both string and Date inputs; please confirm your tRPC transformer (e.g. superjson) is enabled in apps/web/src/server/api/trpc.ts to serialize/deserialize Date fields correctly.
51-79: Mutation flow OK — server returns isWaitlisted and createdAt.Confirmed in apps/web/src/server/api/routers/admin.ts: waitlistUserSelection includes isWaitlisted and createdAt (lines 9–15); findUserByEmail returns that selection (lines 120–123) and updateUserWaitlist returns it as well (lines 136–140).
| if (!isCloud()) { | ||
| return ( | ||
| <div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground"> | ||
| Waitlist tooling is available only in the cloud deployment. | ||
| </div> | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Bug: Cloud-gating likely broken if NEXT_PUBLIC_IS_CLOUD is a string.
isCloud() currently returns env.NEXT_PUBLIC_IS_CLOUD (ref: apps/web/src/utils/common.ts), which can be "true"/"false" (strings). In JS, "false" is truthy, so if (!isCloud()) won’t gate correctly. Fix isCloud() to return a real boolean.
Apply in apps/web/src/utils/common.ts:
+import { env } from "~/env";
-export function isCloud() {
- return env.NEXT_PUBLIC_IS_CLOUD;
-}
+export function isCloud(): boolean {
+ const v = env.NEXT_PUBLIC_IS_CLOUD as unknown;
+ return v === true || String(v).toLowerCase() === "true" || String(v) === "1";
+}Optionally, keep this file unchanged and do a strict check here:
- if (!isCloud()) {
+ if (!(isCloud() === true)) {
return (
<div className="rounded-lg border bg-muted/30 p-6 text-sm text-muted-foreground">
Waitlist tooling is available only in the cloud deployment.
</div>
);
}🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/admin/waitlist/page.tsx lines 92-99 the
cloud-gating relies on isCloud(), but the real fix must be in
apps/web/src/utils/common.ts where isCloud currently returns the raw
NEXT_PUBLIC_IS_CLOUD string; change isCloud to return a boolean by comparing the
env value to the literal "true" (e.g. return env.NEXT_PUBLIC_IS_CLOUD ===
'true') or by parsing it to boolean safely (JSON.parse with try/catch), ensuring
all callers (including this page) get a real boolean and the gate works as
intended.
| } from "@usesend/ui/src/form"; | ||
| import { Input } from "@usesend/ui/src/input"; | ||
| import { Textarea } from "@usesend/ui/src/textarea"; | ||
| import Spinner from "@usesend/ui/src/spinner"; |
There was a problem hiding this comment.
🧩 Analysis chain
Fix incorrect default import for Spinner (build-time error).
packages/ui exports Spinner as a named export.
Apply:
-import Spinner from "@usesend/ui/src/spinner";
+import { Spinner } from "@usesend/ui/src/spinner";Quick check:
🏁 Script executed:
#!/bin/bash
rg -nC2 '^export\s+const\s+Spinner\b' packages/uiLength of output: 307
Fix incorrect default import for Spinner (build-time error).
packages/ui exports Spinner as a named export — change the import to a named import.
File: apps/web/src/app/wait-list/waitlist-form.tsx (around line 17)
Apply:
-import Spinner from "@usesend/ui/src/spinner";
+import { Spinner } from "@usesend/ui/src/spinner";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import Spinner from "@usesend/ui/src/spinner"; | |
| import { Spinner } from "@usesend/ui/src/spinner"; |
🤖 Prompt for AI Agents
In apps/web/src/app/wait-list/waitlist-form.tsx around line 17, the file imports
Spinner as a default export but packages/ui exports it as a named export; change
the import to a named import (e.g., replace the default import with an import
that destructures Spinner from "@usesend/ui/src/spinner") so the build-time
error is resolved.
Summary by CodeRabbit