-
Notifications
You must be signed in to change notification settings - Fork 498
Sign up rules #1138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Sign up rules #1138
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a sign-up rules engine (CEL-based) with evaluation, async analytics, admin-imposed user restriction fields and UI, DB migrations, dashboard rule editor and stats, and updates auth flows to pass sign-up context. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant AuthEndpoint as Auth Endpoint
participant RuleEval as Sign-up Rule Evaluator
participant CELEngine as CEL Engine
participant UserCRUD as User CRUD
participant Database as Database
User->>AuthEndpoint: Sign-up request (email, method, provider?)
AuthEndpoint->>RuleEval: createSignUpRuleContext(...)
RuleEval->>CELEngine: evaluateCelExpression(condition, context)
CELEngine->>CELEngine: Preprocess string methods and evaluate
alt Rule matches
RuleEval->>RuleEval: Apply action (allow/reject/restrict/log)
RuleEval->>AuthEndpoint: Return decision / restricted flag
RuleEval->>Database: Async log sign-up-rule-trigger event
else No match
RuleEval-->>AuthEndpoint: Default action (allow/reject)
end
AuthEndpoint->>UserCRUD: createOrUpgradeAnonymousUserWithRules(..., signUpRuleOptions)
UserCRUD->>Database: Persist user with restricted_by_admin* fields
Database-->>UserCRUD: Persisted user
UserCRUD-->>AuthEndpoint: User object
AuthEndpoint-->>User: Response (success/rejected/restricted)
sequenceDiagram
actor Admin
participant Dashboard as Dashboard UI
participant AdminAPI as Admin API
participant Database as Database
Admin->>Dashboard: Open user → RestrictionDialog
Admin->>Dashboard: Submit public reason + private details
Dashboard->>AdminAPI: PATCH /users/{id} (restricted_by_admin=true, ...)
AdminAPI->>Database: Update ProjectUser
Database-->>AdminAPI: OK
AdminAPI-->>Dashboard: Updated user
Dashboard->>Dashboard: Show RestrictionBanner / RestrictedStatus
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
Greptile OverviewGreptile SummaryThis PR implements a comprehensive signup rules system using CEL (Common Expression Language) expressions to control user registration. The feature allows administrators to define conditions and actions for signup attempts. Key Changes:
Critical Issue Found: The CEL string generation in Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Client
participant API as Auth API
participant Rules as Signup Rules Engine
participant CEL as CEL Evaluator
participant DB as Database
participant Analytics as Analytics Logger
User->>Client: Submit signup (email, password/OAuth)
Client->>API: POST /auth/password/sign-up (or OAuth callback)
API->>Rules: evaluateAndApplySignupRules(tenancy, context)
Rules->>Rules: Get sorted rules (priority, then alphabetical)
loop For each enabled rule
Rules->>CEL: evaluateCelExpression(condition, context)
CEL->>CEL: Preprocess method calls (contains, startsWith, etc.)
CEL->>CEL: Evaluate transformed expression
CEL-->>Rules: true/false
alt Rule matches
Rules->>Analytics: logRuleTrigger() (async, non-blocking)
Analytics->>DB: INSERT INTO SignupRuleTrigger
Rules-->>API: SignupRuleResult (action: reject/restrict/add_metadata/log/allow)
Note over Rules: Stop evaluation after first match
end
end
alt No rules matched
Rules-->>API: Default action (allow/reject)
end
alt Action is "reject"
API-->>Client: 403 SIGN_UP_REJECTED
Client-->>User: Signup rejected
else Action is "restrict"
API->>DB: Create user with restrictedByAdmin=true
DB-->>API: User created
API-->>Client: 200 OK (restricted user)
Client-->>User: Signup successful (restricted)
else Action is "add_metadata"
API->>DB: Create user with metadata in client/server/client_read_only
DB-->>API: User created
API-->>Client: 200 OK
Client-->>User: Signup successful
else Action is "allow" or "log"
API->>DB: Create user normally
DB-->>API: User created
API-->>Client: 200 OK
Client-->>User: Signup successful
end
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5 files reviewed, 1 comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request implements a comprehensive "Sign up rules" feature that allows administrators to configure CEL (Common Expression Language) based rules to control who can sign up for the application. The feature includes rule evaluation, user restriction mechanisms, metadata injection, analytics tracking, and a visual rule builder in the dashboard.
Changes:
- Added CEL-based signup rule evaluation system with support for conditions on email, domain, auth method, and OAuth provider
- Introduced new user restriction fields (
restrictedByAdmin,restrictedByAdminReason,restrictedByAdminPrivateDetails) to support rule-based user restrictions - Implemented analytics tracking for signup rule triggers with a dashboard interface showing rule activity
- Created a visual rule builder UI for non-technical users to create and manage signup rules without writing CEL expressions
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Added dependencies: cel-js (CEL evaluator), @dnd-kit/utilities (drag-and-drop), updated Supabase packages |
| packages/stack-shared/src/config/schema.ts | Added signup rules schema with action types, conditions, priorities, and default action configuration |
| packages/stack-shared/src/known-errors.tsx | Added SignUpRejected error for rule-based signup rejection |
| packages/stack-shared/src/interface/crud/users.ts | Extended user CRUD schemas with admin restriction fields and validation |
| packages/template/src/lib/stack-app/users/index.ts | Added restrictedByAdmin fields to user types and updated restriction reason types |
| apps/backend/src/lib/cel-evaluator.ts | Implemented CEL expression evaluation with method call preprocessing for string operations |
| apps/backend/src/lib/signup-rules.ts | Core signup rule evaluation logic with priority-based rule matching |
| apps/backend/src/lib/users.tsx | Added createOrUpgradeAnonymousUserWithRules wrapper for signup rule evaluation |
| apps/backend/prisma/schema.prisma | Added restrictedByAdmin fields to ProjectUser and SignupRuleTrigger table for analytics |
| apps/backend/prisma/migrations/* | Database migrations for admin restriction fields and signup rule triggers |
| apps/backend/src/app/api/latest/internal/signup-rules/route.tsx | Analytics endpoint for fetching signup rule trigger statistics |
| apps/backend/src/app/api/latest/auth/*/route.tsx | Integrated signup rule evaluation into all auth signup endpoints |
| apps/dashboard/src/lib/cel-visual-parser.ts | Visual CEL expression parser for converting between tree structure and CEL strings |
| apps/dashboard/src/components/rule-builder/* | React components for visual rule building interface |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/signup-rules/* | Dashboard page for managing signup rules with drag-and-drop reordering |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx | Added UI for viewing and managing user admin restrictions |
| apps/e2e/tests/backend/endpoints/api/v1/auth/signup-rules.test.ts | Comprehensive test suite covering rule evaluation, priorities, actions, and edge cases |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apps/backend/prisma/migrations/20260201400001_add_restricted_by_admin_constraint/migration.sql
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/components/rule-builder/condition-builder.tsx
Outdated
Show resolved
Hide resolved
apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/backend/src/app/api/latest/users/crud.tsx (1)
100-122: Filter out admin-restricted users wheninclude_restricted=false.
computeRestrictedStatustreatsrestrictedByAdminas a restricted condition (alongsideisAnonymousand email verification), but the list endpoint only filters based on email verification. This means users markedrestrictedByAdmin: truewill appear in results wheninclude_restricted=false, creating an inconsistency. The function itself notes: "when you implement this function, make sure to also update the filter in the list users endpoint."Add
restrictedByAdmin: falseto the where clause when!includeRestricted:+ const shouldFilterRestrictedByAdmin = !includeRestricted; const where = { tenancyId: auth.tenancy.id, ... + ...shouldFilterRestrictedByAdmin ? { + restrictedByAdmin: false, + } : {}, ...shouldFilterRestrictedByEmail ? {
🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/internal/signup-rules/route.tsx`:
- Around line 59-101: The hourly_counts output omits hours with zero activity,
which skews per-hour averages and sparklines; update the aggregation to produce
a complete 48-hour series per rule: after building ruleData.hourlyMap in the
loop that processes triggers (symbols: triggers, trigger.triggeredAt,
ruleTriggersMap, ruleData.hourlyMap), compute the 48 hourly keys (e.g., ending
at now or the latest triggeredAt) and map each key to
ruleData.hourlyMap.get(key) ?? 0, then use that full array when constructing
ruleTriggers (symbol: ruleTriggers) so hourly_counts always contains 48 hour
entries (hour + count) in sorted order. Ensure the hour key format matches the
existing ISO/truncated format used by hourKey.
In `@apps/backend/src/lib/cel-evaluator.ts`:
- Around line 76-83: The 'matches' branch in cel-evaluator.ts constructs a
RegExp from user input (arg) and runs test(varValue), which can trigger ReDoS;
change this to validate or sandbox regexes before executing: either (a) use a
safe-regex check on arg (e.g., safeRegex(arg)) and skip/deny unsafe patterns, or
(b) run the regex.test in a short-timeout sandbox (measure time around RegExp
execution or offload to a worker with a timeout) and treat over-time executions
as failures; update the case 'matches' handler to reference arg, varValue and
result and ensure exceptions/timeouts set result = false and log the offending
pattern.
In `@apps/backend/src/lib/signup-rules.ts`:
- Around line 64-83: The logRuleTrigger function is persisting raw PII
(context.email, emailDomain, authMethod, oauthProvider) into analytics; update
the metadata construction in logRuleTrigger so that email is pseudonymized
(e.g., hash the email using a secure one-way hash like SHA-256 with a salt or
redact it) and only store non-identifying fields needed for rules (e.g., store a
hashedEmail and/or emailDomainHash instead of plain email/emailDomain), keep
authMethod and oauthProvider if non-identifying or map them to enums, and
add/modify a retention/note field documenting that these fields are
pseudonymized and how long they are kept; locate changes around the
globalPrismaClient.signupRuleTrigger.create call and the metadata object in
logRuleTrigger to implement hashing/redaction and retention documentation.
- Around line 123-130: The sort can produce NaN when a rule's optional priority
is undefined; update the extraction inside the comparator used by
sortedRuleEntries to assert presence instead of silently falling back: for both
priorityA and priorityB (derived from rules entries in sortedRuleEntries)
replace direct property access with a nullish-coalescing assertion (e.g., use
priority = entry[1].priority ?? throwErr(...) or throw new Error(...)) so
missing priorities throw (reference SignupRuleConfig for the field) and keep the
tie-breaker using stringCompare(a[0], b[0]) unchanged.
In `@apps/backend/src/lib/users.tsx`:
- Around line 5-6: The code is silently falling back to an empty string for
primary_email when evaluating signup rules; instead, update the evaluation call
sites (e.g., where createSignupRuleContext and evaluateAndApplySignupRules /
SignupRuleMetadataEntry are used) to fail fast if primary_email is missing by
throwing a clear error (use a helper like throwErr or replace the ""/'' fallback
with `primary_email ?? throwErr("primary_email required for signup rule
evaluation")`), and adjust the input types or checks at the top-level so callers
must provide primary_email (also fix similar fallbacks around the other
occurrences at the block referenced by lines 45-47).
In `@apps/dashboard/package.json`:
- Around line 24-26: The review notes that while `@dnd-kit/core`,
`@dnd-kit/sortable`, and `@dnd-kit/utilities` are compatible, the package
react-resizable-panels should be bumped to avoid React 19 peer-dep warnings;
update the dependency entry for react-resizable-panels in package.json to ^2.1.7
(or later) so its peerDependencies explicitly declare React 19 support, then run
yarn/npm install and test to ensure no new warnings or breakages.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/signup-rules/page-client.tsx:
- Around line 112-115: avgPerHour is computed using data.length which only
counts active-hour buckets and inflates the per-hour rate; change the divisor to
the full fixed window (48) or use a provided totalHours field from the API if
available. Locate avgPerHour and rateLabel (where avgPerHour = totalCount /
Math.max(data.length, 1) and rateLabel is set) and replace Math.max(data.length,
1) with a constant 48 (or use totalHours) so avgPerHour reflects the full 48h
window before formatting into rateLabel.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/signup-rules/page.tsx:
- Around line 1-6: Remove the incorrect "use server" directive at the top of the
file — leave the file as an async Server Component by default; keep the import
of PageClient and the export default async function Page() that returns
<PageClient /> (so update the module to just import PageClient and export the
Page function without any "use server" or other directives).
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 309-335: Both handlers (handleSaveAndRestrict and
handleRemoveRestriction) currently swallow update errors via try/finally; wrap
the await user.update(...) in a try/catch inside each function so errors are
caught, display a blocking error alert with the caught error message (e.g.,
using the app's alert/toast API or window.alert) and then rethrow or return
after alerting so the error is not silently swallowed; keep the existing finally
block to call setIsSaving(false) and only call onOpenChange(false) on success
(move it into the try after await).
In `@apps/dashboard/src/components/rule-builder/condition-builder.tsx`:
- Around line 146-155: The icon-only remove Button in condition-builder.tsx (the
Button rendering TrashIcon when showRemove is true) lacks an accessible name;
update the Button(s) that call onRemove to include an aria-label (e.g.,
aria-label="Remove condition" or similar) or add visually hidden text so screen
readers announce the action, and apply the same change to the other remove
Button instance in this file (the second TrashIcon/Button rendering that also
invokes onRemove).
- Around line 305-318: The current render mutates the incoming prop by aliasing
value to rootGroup and assigning rootGroup.children = [createEmptyCondition()],
which mutates parent state; instead, avoid mutation in render by keeping the
existing rootGroup logic pure (use spreading/cloning when building a non-group
root) and move the "ensure default condition" logic into a useEffect that runs
when value changes: inside useEffect, if value.type === 'group' and
value.children.length === 0, call onChange with a new GroupNode (copying value
but setting children: [createEmptyCondition()]); reference rootGroup, value,
createEmptyCondition, and onChange when implementing the effect to sync state
without direct mutation.
In `@apps/dashboard/src/lib/cel-visual-parser.ts`:
- Around line 66-99: The generated CEL strings in conditionToCel produce invalid
or unsafe output when value contains quotes or backslashes; add an escape helper
(e.g., escapeCelString) and use it wherever value is interpolated: escape double
quotes and backslashes and coerce non-string values to strings before escaping;
update the in_list branch to map through escapeCelString for each element and
join, and replace all direct `"${value}"` interpolations in conditionToCel with
`"${escapeCelString(value)}"` so CEL syntax remains valid and prevents
injection.
In `@packages/stack-shared/src/config/schema.ts`:
- Around line 138-147: The signupRuleSchema allows enabled rules with empty or
undefined condition; update signupRuleSchema so the condition field is required
and non-empty when enabled is true — use yup's conditional validation (yup.when
on 'enabled') on the condition string to call required() and enforce a non-empty
trimmed string (e.g., min(1) or test for non-blank) when enabled === true,
leaving the condition optional when enabled is false or undefined; modify the
existing signupRuleSchema's condition entry accordingly to reference the
'enabled' flag.
- Around line 126-129: The signupRuleMetadataEntrySchema currently uses
yupMixed<string | number | boolean>() for the value field which does not enforce
runtime scalar types; update the value validator in
signupRuleMetadataEntrySchema (the value key) to explicitly assert scalar types
at runtime — replace the generic mixed with a mixed().test (or a lazy/oneOf
schema) that checks typeof value is 'string' or 'number' or 'boolean' (and
rejects arrays/objects/null/undefined), and keep .defined() so downstream code
that expects scalar metadata cannot receive arrays/objects; reference the value
field and signupRuleMetadataEntrySchema when making the change.
🧹 Nitpick comments (14)
docker/dev-postgres-with-extensions/Dockerfile (1)
56-56: Consider removing backticks for clarity; use a Dockerfile comment instead.While backticks with
#comments are technically parsed by/bin/shwithout error (the#creates a comment that expands to an empty string), this syntax is unconventional and confusing. Moving the note outside the command string makes the intent clearer:Suggested change
- -c statement_timeout=30s `# In production this is higher, but better safe than sorry during dev` \ + -c statement_timeout=30s \ + # In production this is higher, but better safe than sorry during devpackages/stack-shared/src/config/schema.ts (1)
131-136: Make action-specific fields conditional.
metadataandmessageare currently allowed for any action type, which weakens validation. Consider requiring them only when relevant.♻️ Suggested tightening
const signupRuleActionSchema = yupObject({ type: yupString().oneOf(['allow', 'reject', 'restrict', 'log', 'add_metadata']).defined(), - metadata: yupRecord(yupString(), signupRuleMetadataEntrySchema).optional(), - message: yupString().optional(), // for reject action custom message (internal use, not shown to user) + metadata: yupRecord(yupString(), signupRuleMetadataEntrySchema).when("type", { + is: "add_metadata", + then: s => s.defined(), + otherwise: s => s.optional(), + }), + message: yupString().when("type", { + is: "reject", + then: s => s.defined(), + otherwise: s => s.optional(), + }), // for reject action custom message (internal use, not shown to user) });apps/backend/src/lib/cel-evaluator.ts (1)
178-192: Edge case: emails without@symbol.Line 184 handles emails without
@by returning an empty string foremailDomain, which is reasonable. However, consider whether this edge case should be validated earlier in the signup flow rather than silently producing an empty domain.export function createSignupRuleContext(params: { email: string, authMethod: 'password' | 'otp' | 'oauth' | 'passkey', oauthProvider?: string, }): SignupRuleContext { const email = params.email; - const emailDomain = email.includes('@') ? email.split('@').pop() ?? '' : ''; + const atIndex = email.lastIndexOf('@'); + const emailDomain = atIndex !== -1 ? email.slice(atIndex + 1) : ''; return { email, emailDomain, authMethod: params.authMethod, oauthProvider: params.oauthProvider ?? '', }; }packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)
442-455: Avoid unsound cast for admin-restriction fields.The assertion bypasses UsersCrud typing and can silently propagate
undefinedif client/server versions drift. Prefer updating shared CRUD types and/or guarding the fields to fail fast when missing.♻️ Suggested guard (fails fast on missing fields)
- const crudWithAdminRestriction = crud as typeof crud & { - restricted_by_admin: boolean, - restricted_by_admin_reason: string | null, - restricted_by_admin_private_details: string | null, - }; + const crudWithAdminRestriction = crud as typeof crud & { + restricted_by_admin?: boolean, + restricted_by_admin_reason?: string | null, + restricted_by_admin_private_details?: string | null, + }; @@ - restrictedByAdmin: crudWithAdminRestriction.restricted_by_admin, - restrictedByAdminReason: crudWithAdminRestriction.restricted_by_admin_reason, - restrictedByAdminPrivateDetails: crudWithAdminRestriction.restricted_by_admin_private_details, + restrictedByAdmin: crudWithAdminRestriction.restricted_by_admin ?? throwErr("restricted_by_admin missing — update UsersCrud types/server response"), + restrictedByAdminReason: crudWithAdminRestriction.restricted_by_admin_reason ?? null, + restrictedByAdminPrivateDetails: crudWithAdminRestriction.restricted_by_admin_private_details ?? null,As per coding guidelines: Never silently use fallback values when type errors occur. Update types or throw errors instead. Use ?? throwErr(...) over non-null assertions with clear error messages explaining the assumption.
apps/dashboard/src/components/rule-builder/condition-builder.tsx (1)
44-47: PreferMapforPREDEFINED_VALUES.This aligns with the project guideline and keeps lookups explicit.
♻️ Suggested refactor
-const PREDEFINED_VALUES: Partial<Record<ConditionField, string[]>> = { - authMethod: ['password', 'otp', 'oauth', 'passkey'], - oauthProvider: ['google', 'github', 'microsoft', 'facebook', 'discord', 'apple', 'linkedin', 'gitlab', 'bitbucket', 'spotify', 'twitch', 'x'], -}; +const PREDEFINED_VALUES = new Map<ConditionField, string[]>([ + ['authMethod', ['password', 'otp', 'oauth', 'passkey']], + ['oauthProvider', ['google', 'github', 'microsoft', 'facebook', 'discord', 'apple', 'linkedin', 'gitlab', 'bitbucket', 'spotify', 'twitch', 'x']], +]); @@ - const predefinedValues = PREDEFINED_VALUES[condition.field]; + const predefinedValues = PREDEFINED_VALUES.get(condition.field);As per coding guidelines: Use ES6 maps instead of records wherever possible.
Also applies to: 62-63
apps/backend/src/app/api/latest/internal/signup-rules/route.tsx (1)
47-57: Consider DB-side aggregation for high-volume tenants.
findManypulls all triggers for 48h and aggregates in JS. For large tenants this can be expensive. Consider selecting only needed columns and using PrismagroupByor a rawdate_trunc('hour', triggeredAt)query to aggregate by rule/action/hour to reduce memory and latency.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/signup-rules/page-client.tsx (2)
625-653: Avoid try/catch-all in analytics fetch.The fetch uses a broad
catchandconsole.debug. PreferrunAsynchronously/runAsynchronouslyWithAlertwith anonErrorhandler and keepfinallyforisLoadingso errors are surfaced without a catch-all.🔧 Example refactor
- const fetchAnalytics = async () => { - try { - const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/signup-rules', { - method: 'GET', - }); - if (cancelled) return; - - const data = await response.json(); - ... - setAnalytics(analyticsMap); - } catch (e) { - console.debug('Failed to fetch signup rules analytics:', e); - } finally { - if (!cancelled) { - setIsLoading(false); - } - } - }; - - runAsynchronously(fetchAnalytics()); + runAsynchronously(async () => { + try { + const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/signup-rules', { + method: 'GET', + }); + if (cancelled) return; + const data = await response.json(); + ... + setAnalytics(analyticsMap); + } finally { + if (!cancelled) setIsLoading(false); + } + }, { + onError: (error) => { + // handle/report error here + }, + });As per coding guidelines, "Never try-catch-all, never void a promise, and never .catch(console.error) or similar. Use loading indicators instead when UI is involved. If async is necessary, use runAsynchronously or runAsynchronouslyWithAlert".
630-632: Avoid theanycast for StackApp internals.Line 630:
(stackAdminApp as any)[stackAppInternalsSymbol]drops type safety. Consider a typed helper for the internal request channel, or add a short comment explaining whyanyis unavoidable here.As per coding guidelines, "Avoid the
anytype. When necessary, leave a comment explaining why it's being used and why the type system fails at that point".apps/backend/src/lib/signup-rules.ts (1)
64-89: Avoid broad try/catch + console.error in rule logging/eval.Both
logRuleTriggerand the rule-eval loop catch-all and log to console. SincerunAsynchronouslyalready captures rejections andevaluateCelExpressionreturnsfalseon error, prefer centralized error handling instead of catch-all blocks.As per coding guidelines, "Never try-catch-all, never void a promise, and never .catch(console.error) or similar. Use loading indicators instead when UI is involved. If async is necessary, use runAsynchronously or runAsynchronouslyWithAlert".
Also applies to: 154-157
apps/e2e/tests/backend/endpoints/api/v1/auth/signup-rules.test.ts (2)
1524-1529: Encode interpolated path segments in URLs.When building
/api/v1/users/${userId}, useencodeURIComponent(userId)(orurlString) to keep URL handling consistent and safe.🔧 Suggested tweak
- const userResponse = await niceBackendFetch(`/api/v1/users/${userId}`, { + const userResponse = await niceBackendFetch(`/api/v1/users/${encodeURIComponent(userId)}`, { method: "GET", accessType: "admin", });As per coding guidelines, "Use
urlStringtemplate literals orencodeURIComponent()instead of normal string interpolation for URLs, for consistency".
81-84: Prefer inline snapshots for response bodies.For structured error payloads, inline snapshots keep expectations compact and aligned with test conventions.
🔧 Example
- expect(response.body).toMatchObject({ - code: 'SIGN_UP_REJECTED', - }); + expect(response.body).toMatchInlineSnapshot(` + { + "code": "SIGN_UP_REJECTED", + } + `);As per coding guidelines, "Prefer .toMatchInlineSnapshot over other selectors when writing tests. Check snapshot-serializer.ts to understand how snapshots are formatted".
apps/dashboard/src/lib/cel-visual-parser.ts (3)
248-261: Consider validating the parsed field against known ConditionField values.The regex matches any word character sequence for the field name, then casts it to
ConditionFieldwithout validation. If the CEL contains an unrecognized field, it will be silently accepted with an invalid type.♻️ Proposed validation helper
+const VALID_FIELDS: readonly ConditionField[] = ['email', 'emailDomain', 'authMethod', 'oauthProvider']; + +function isValidField(field: string): field is ConditionField { + return VALID_FIELDS.includes(field as ConditionField); +} + function parseCondition(expr: string): ConditionNode | null { const trimmed = expr.trim(); // Match patterns like: field == "value" const equalsMatch = trimmed.match(/^(\w+)\s*==\s*"([^"]*)"$/); - if (equalsMatch) { + if (equalsMatch && isValidField(equalsMatch[1])) { return { type: 'condition', id: generateNodeId(), - field: equalsMatch[1] as ConditionField, + field: equalsMatch[1], operator: 'equals', value: equalsMatch[2], }; } // Apply similar pattern to other matches...
323-343: In-list parsing does not handle values containing commas.The simple comma split will incorrectly parse
["a,b", "c"]as three items instead of two. This is a known limitation that likely won't affect typical signup rule values (emails, domains, providers), but worth documenting.Consider adding a comment noting this limitation, or implementing a proper tokenizer if comma-containing values become a requirement.
394-410: Type cast inupdateNodeInTreecould mask incompatible updates.The
Partial<RuleNode>type allows passing properties from eitherConditionNodeorGroupNode, and theas RuleNodecast will accept incompatible combinations (e.g., updating a condition node withchildren). Consider narrowing the type or adding runtime validation.♻️ Type-safe alternative using overloads
export function updateNodeInTree(tree: RuleNode, nodeId: string, updates: Partial<ConditionNode>): RuleNode; export function updateNodeInTree(tree: RuleNode, nodeId: string, updates: Partial<GroupNode>): RuleNode; export function updateNodeInTree(tree: RuleNode, nodeId: string, updates: Partial<ConditionNode | GroupNode>): RuleNode { if (tree.id === nodeId) { // Optionally validate that updates.type matches tree.type if present return { ...tree, ...updates } as RuleNode; } // ... rest unchanged }
apps/dashboard/src/components/rule-builder/condition-builder.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx:
- Around line 144-223: The metadata editor currently stringifies all metadata on
load (see MetadataEditorEntry and initialMetadata) causing type loss; update the
MetadataEditorEntry shape to include the original type (e.g., { key, value,
target, valueType }) and change initialMetadata to store the raw value and its
type instead of String(entry.value); keep metadataEntries state as these richer
entries; then update handleSave to reconstruct SignUpRuleMetadataEntry values by
converting the string-edited value back to the original valueType (or reusing
the raw value if untouched) before building the metadata object so
numbers/booleans are preserved when saving.
- Around line 360-369: The onClick currently passes the async handler handleSave
directly which can void the returned promise and swallow rejections; update the
Button to call runAsynchronouslyWithAlert(() => handleSave()) (or wrap
handleSave in an inline async wrapper passed to runAsynchronouslyWithAlert) so
any rejection is caught and surfaced to the user; locate the Button using the
handleSave reference in page-client.tsx and replace onClick={handleSave} with
the wrapped invocation using runAsynchronouslyWithAlert.
- Around line 628-629: The fetchAnalytics function currently wraps its network
call in a try-catch that swallows errors; remove the internal catch so errors
propagate, and when invoking it use runAsynchronouslyWithAlert(fetchAnalytics())
so failures surface via the alert helper; keep the existing finally block in
fetchAnalytics to reset loading state (refer to the fetchAnalytics function and
the runAsynchronouslyWithAlert helper).
🧹 Nitpick comments (8)
apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts (1)
1472-1484: Consider documenting expected normalized behavior.The test currently accepts both 200 and 403 for uppercase "ADMIN", which documents uncertainty about email normalization. If the system has a defined normalization behavior (lowercase), consider updating this test to assert the expected outcome once behavior is confirmed.
apps/backend/src/lib/cel-evaluator.ts (2)
17-21: Remove unused_email_lowerfrom ExtendedContext.The
ExtendedContexttype defines_email_lowerbut this field is never populated or used anywhere in the code. Either implement it for case-insensitive matching or remove the dead code.♻️ Proposed fix
-// Extended context with helper functions for string operations -type ExtendedContext = SignUpRuleContext & { - // Pre-computed helpers for common patterns - _email_lower: string, -};
41-43: Regex pattern doesn't handle single quotes or escaped characters.The pattern only matches double-quoted string arguments like
email.contains("test"). Single-quoted stringsemail.contains('test')and escaped quotesemail.contains("test\"value")won't be processed.♻️ Enhanced pattern (optional)
- const methodPattern = /(\w+)\.(contains|startsWith|endsWith|matches)\s*\(\s*"([^"]+)"\s*\)/g; + // Match both single and double quoted strings, handling basic escapes + const methodPattern = /(\w+)\.(contains|startsWith|endsWith|matches)\s*\(\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')\s*\)/g;Note: If CEL expressions are admin-controlled and documented to use double quotes only, the current implementation is sufficient.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)
292-294: Multipleas anycasts indicate incomplete type definitions.The
ServerUsertype doesn't includerestrictedByAdmin,restrictedByAdminReason, andrestrictedByAdminPrivateDetails, requiringas anycasts throughout the component. This bypasses type safety and could lead to runtime errors if the API changes.Consider either:
- Extending the
ServerUsertype to include these fields (if they're part of the stable API)- Creating a local extended type until the SDK types are updated
// Temporary type extension until SDK types are updated type ServerUserWithRestriction = ServerUser & { restrictedByAdmin?: boolean; restrictedByAdminReason?: string | null; restrictedByAdminPrivateDetails?: string | null; };Also applies to: 435-437
apps/backend/src/app/api/latest/internal/sign-up-rules/route.tsx (1)
74-79: Action type safety relies on runtime behavior.The cast
trigger.action as keyof typeof actionCountsassumes the database always contains valid action values. If an unexpected action is stored, it would silently be ignored due to theif (action in actionCounts)check.Consider logging or capturing unexpected action values for debugging:
const action = trigger.action as keyof typeof actionCounts; if (action in actionCounts) { actionCounts[action]++; + } else { + // Log unexpected action for debugging + console.warn(`Unexpected signup rule action: ${trigger.action}`); }apps/backend/src/lib/sign-up-rules.ts (2)
86-89: Avoidconsole.errorin catch blocks per coding guidelines.The coding guidelines state to never use
.catch(console.error) or similar. Consider using a proper logging infrastructure that can be monitored/alerted on, or propagate the error if it should be visible. If truly swallowing errors intentionally, at minimum use a structured logger.♻️ Suggested approach
} catch (e) { - // Don't fail the signup if logging fails - console.error('Failed to log sign-up rule trigger:', e); + // Don't fail the signup if logging fails - use structured logging + // TODO: Replace with proper logging infrastructure (e.g., logger.error) + void e; // Intentionally swallowed - analytics logging is non-critical }Or better, if a logging service exists:
} catch (e) { logger.error('Failed to log sign-up rule trigger', { error: e, tenancyId, ruleId }); }Based on learnings: "Never try-catch-all, never void a promise, and never .catch(console.error) or similar."
154-157: Avoidconsole.errorin catch blocks per coding guidelines.Similar to the earlier catch block,
console.errorshould be replaced with proper structured logging. CEL evaluation errors may indicate misconfigured rules that admins should be alerted about.♻️ Suggested approach
} catch (e) { - // Log CEL evaluation error but continue to next rule - console.error(`CEL evaluation error for rule ${ruleId}:`, e); + // Log CEL evaluation error but continue to next rule + // TODO: Use structured logging for monitoring/alerting on rule misconfigurations + // logger.warn('CEL evaluation error', { ruleId, error: e, tenancyId: tenancy.id }); }Based on learnings: "Never try-catch-all, never void a promise, and never .catch(console.error) or similar."
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
630-632: Replaceas anywith a narrow internal type.The
stackAppInternalsSymbolpattern provides internal SDK access but requiresas anyto bypass type checking. This can be avoided by defining a type for the internals object, improving type safety and maintainability.♻️ Suggested change
const stackAppInternalsSymbol = Symbol.for("StackAuth--DO-NOT-USE-OR-YOU-WILL-BE-FIRED--StackAppInternals"); +type StackAppInternals = { + sendRequest: (path: string, init: { method: string }) => Promise<Response>; +}; - const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', { + const internals = (stackAdminApp as { [stackAppInternalsSymbol]: StackAppInternals })[stackAppInternalsSymbol]; + const response = await internals.sendRequest('/internal/sign-up-rules', { method: 'GET', });
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/components/rule-builder/condition-builder.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/components/rule-builder/condition-builder.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/dashboard/src/lib/cel-visual-parser.ts`:
- Around line 259-351: The condition parsers (equalsMatch, notEqualsMatch,
matchesMatch, endsWithMatch, startsWithMatch, containsMatch, inListMatch)
currently use [^"]* so escaped quotes/backslashes (e.g. \" or \\) are not
unescaped and round‑trip is broken; add an unescapeCelString helper (e.g.
unescape sequences like \\ and \") and change each regex to capture escaped
sequences using ((?:\\.|[^"\\])*) instead of ([^"]*), then call
unescapeCelString(...) on the captured value when building the returned
condition objects and on each quoted item in the inListMatch items; ensure
generateNodeId usage remains the same.
🧹 Nitpick comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)
293-295: Avoidanyfor restriction fields; make the new properties explicit.Casting
usertoanyand defaultingrestrictedByAdmin*values hides type gaps and can silently mask missing data. Please extendServerUser(or use a narrowed local type) and access these fields withoutany, using an explicit guard if absence is unexpected.🧩 Example approach (local narrowing)
+type ServerUserRestrictionFields = { + restrictedByAdmin?: boolean; + restrictedByAdminReason?: string | null; + restrictedByAdminPrivateDetails?: string | null; +}; +const restrictionUser = user as ServerUser & ServerUserRestrictionFields; -const restrictedByAdmin = (user as any).restrictedByAdmin ?? false; -const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null; -const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null; +const restrictedByAdmin = restrictionUser.restrictedByAdmin ?? false; +const restrictedByAdminReason = restrictionUser.restrictedByAdminReason ?? null; +const restrictedByAdminPrivateDetails = restrictionUser.restrictedByAdminPrivateDetails ?? null;As per coding guidelines: Avoid the
anytype. When necessary, leave a comment explaining why it's being used and why the type system fails at that point; Never silently use fallback values when type errors occur. Update types or throw errors instead. Use?? throwErr(...)over non-null assertions with clear error messages explaining the assumption.Also applies to: 440-442
apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts (1)
81-84: Consider inline snapshots for structured response assertions.For response bodies like
SIGN_UP_REJECTED, inline snapshots improve readability and make regressions more visible during test updates.As per coding guidelines: Prefer .toMatchInlineSnapshot over other selectors when writing tests. Check snapshot-serializer.ts to understand how snapshots are formatted.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
617-633: Avoidanywhen accessing SDK internals.Using
(stackAdminApp as any)discards type safety. Consider a minimal internal interface (or a helper accessor) so the unsafe surface is localized and documented.🧩 Minimal narrowing
+type StackAdminAppInternals = { + sendRequest: (path: string, init: RequestInit) => Promise<Response>; +}; +const internals = (stackAdminApp as unknown as Record<typeof stackAppInternalsSymbol, StackAdminAppInternals>)[stackAppInternalsSymbol]; -const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', { +const response = await internals.sendRequest('/internal/sign-up-rules', { method: 'GET', });As per coding guidelines: Avoid the
anytype. When necessary, leave a comment explaining why it's being used and why the type system fails at that point.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/backend/package.json`:
- Line 117: Remove the redundant "@types/re2" dependency from package.json:
locate the dependency entry "@types/re2": "^1.10.8" and delete it, then run your
package manager (npm/yarn/pnpm) to update lockfile and reinstall so the project
uses the built-in types from "re2" (v1.23.1) instead of the deprecated `@types`
package.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx:
- Around line 516-531: The edit and delete icon-only Buttons (the Button
components wrapping PencilSimpleIcon and TrashIcon with onClick handlers onEdit
and onDelete) lack accessible names; add clear aria-label attributes (or a
visually hidden text node) to each Button such as aria-label="Edit rule" and
aria-label="Delete rule" (or include the rule name dynamically) so screen
readers can announce the action; update the Button instances that render
PencilSimpleIcon and TrashIcon to include these aria labels.
- Around line 337-345: The trash icon button used to remove metadata entries
(Button with onClick={() => removeMetadataEntry(index)} and
disabled={metadataEntries.length <= 1}) lacks an accessible name; update the
Button to include an accessible label (for example add aria-label={`Remove
metadata entry ${index + 1}`} or a title prop, or wrap a visually hidden text
inside the button) so screen readers can announce its purpose while preserving
the visual icon-only appearance.
🧹 Nitpick comments (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (2)
293-295: Add comments explaining theas anycasts.Per coding guidelines, when using
anytype, leave a comment explaining why it's being used. These casts suggest therestrictedByAdmin*fields aren't yet in theServerUsertype definition.📝 Suggested documentation
- const restrictedByAdmin = (user as any).restrictedByAdmin ?? false; - const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null; - const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null; + // TODO: Remove `as any` once ServerUser type includes admin restriction fields + const restrictedByAdmin = (user as any).restrictedByAdmin ?? false; + const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null; + const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null;
440-442: Consider extracting admin restriction field access to avoid duplication.The same
(user as any).restrictedByAdmin*pattern appears in bothRestrictionDialog(lines 293-295) andRestrictionBanner(lines 440-442). Consider extracting a helper function to reduce duplication and centralize the type workaround.♻️ Suggested helper
// Helper to access admin restriction fields until ServerUser type is updated function getAdminRestrictionFields(user: ServerUser) { // TODO: Remove `as any` once ServerUser type includes these fields const u = user as any; return { restrictedByAdmin: u.restrictedByAdmin ?? false, restrictedByAdminReason: u.restrictedByAdminReason ?? null, restrictedByAdminPrivateDetails: u.restrictedByAdminPrivateDetails ?? null, }; }apps/dashboard/src/lib/cel-visual-parser.test.ts (1)
131-151: Consider using inline snapshots and strengthen parsing test assertions.The parsing tests use conditional checks that silently pass if the result type isn't
'condition'. If parsing returns an unexpected type, the inner assertions never run and the test still passes. Per coding guidelines, prefer.toMatchInlineSnapshotfor test assertions.♻️ Suggested refactor using inline snapshots
describe('CEL to visual tree parsing', () => { it('should parse simple equality condition', () => { const result = parseCelToVisualTree('email == "[email protected]"'); - expect(result).toBeDefined(); - if (result?.type === 'condition') { - expect(result.field).toBe('email'); - expect(result.operator).toBe('equals'); - expect(result.value).toBe('[email protected]'); - } + expect(result).toMatchInlineSnapshot(` + { + "field": "email", + "id": expect.any(String), + "operator": "equals", + "type": "condition", + "value": "[email protected]", + } + `); }); it('should parse endsWith condition', () => { const result = parseCelToVisualTree('email.endsWith("@gmail.com")'); - expect(result).toBeDefined(); - if (result?.type === 'condition') { - expect(result.field).toBe('email'); - expect(result.operator).toBe('ends_with'); - expect(result.value).toBe('@gmail.com'); - } + expect(result).toMatchInlineSnapshot(` + { + "field": "email", + "id": expect.any(String), + "operator": "ends_with", + "type": "condition", + "value": "@gmail.com", + } + `); }); });apps/dashboard/src/components/rule-builder/condition-builder.tsx (1)
114-124: Thein_listinput loses intermediate edits while typing.Splitting on comma and filtering empty strings on every keystroke removes trailing commas immediately, making it awkward to type lists. Users can't type
"value1, "before adding the second value.♻️ Consider debouncing or only processing on blur
{condition.operator === 'in_list' ? ( <input type="text" - value={Array.isArray(condition.value) ? condition.value.join(', ') : condition.value} - onChange={(e) => { - const items = e.target.value.split(',').map(s => s.trim()).filter(Boolean); - handleValueChange(items); - }} + value={Array.isArray(condition.value) ? condition.value.join(', ') : condition.value} + onChange={(e) => { + // Store raw input while typing + const rawValue = e.target.value; + // Only split/filter on blur or when there's a complete item + const items = rawValue.split(',').map(s => s.trim()); + handleValueChange(items.filter(Boolean)); + }} + onBlur={(e) => { + // Clean up on blur + const items = e.target.value.split(',').map(s => s.trim()).filter(Boolean); + handleValueChange(items); + }} placeholder="value1, value2, ..." className="h-8 px-2 text-sm bg-background/60 border border-border/50 rounded-md flex-1" />apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
629-632: Add comment explaining theanycast for SDK internals access.Per coding guidelines, avoid the
anytype. When necessary, leave a comment explaining why it's being used.♻️ Suggested fix
const fetchAnalytics = async () => { try { + // Using `any` cast to access internal SDK method via Symbol. + // The public SDK API doesn't expose this endpoint, so we access internals directly. + // eslint-disable-next-line `@typescript-eslint/no-explicit-any` const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', { method: 'GET', });
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/users/crud.tsx`:
- Around line 618-622: Add the same validation in the onCreate flow that
requires restricted_by_admin_reason and restricted_by_admin_private_details to
only be set when restricted_by_admin is true: before calling
tx.projectUser.create in the onCreate handler, check the incoming data fields
(restricted_by_admin, restricted_by_admin_reason,
restricted_by_admin_private_details) and throw/return a validation error if
reason or private_details are non-empty while restricted_by_admin is false;
mirror the logic used in onUpdate so tx.projectUser.create only receives
reason/details when restricted_by_admin === true.
- Around line 1089-1100: The current validation uses data.restricted_by_admin
!== true and ignores the existing oldUser.restrictedByAdmin, breaking PATCHes
that only update reason fields; change the check to compute an effective flag
(e.g., effectiveRestricted = data.restricted_by_admin ??
oldUser.restrictedByAdmin) and use that in the logic so that when
effectiveRestricted !== true you throw if restrictedByAdminReason or
restrictedByAdminPrivateDetails are provided, and when effectiveRestricted ===
false you clear restrictedByAdminReason and restrictedByAdminPrivateDetails;
update the branch that references data.restricted_by_admin,
oldUser.restrictedByAdmin, restrictedByAdminReason, and
restrictedByAdminPrivateDetails accordingly.
In `@apps/backend/src/lib/sign-up-rules.ts`:
- Around line 118-121: The current code only records rule triggers when
action.type === 'reject', so matching rules with action.type === 'log' are never
sent to analytics; update the branch around
runAsynchronously(logRuleTrigger(tenancy.id, ruleId, context, action)) to also
invoke logRuleTrigger for 'log' actions (e.g., change the condition to if
(action.type === 'reject' || action.type === 'log') or otherwise call
runAsynchronously(logRuleTrigger(...)) for both reject and log) while keeping
the async fire-and-forget behavior and using the existing identifiers
(runAsynchronously, logRuleTrigger, tenancy.id, ruleId, context, action).
- Around line 128-131: The catch-all around CEL evaluation (the block logging
`CEL evaluation error for rule ${ruleId}`) must be tightened: only swallow known
CEL evaluation errors and log them with the structured logger (replace
console.error with the project's logger, e.g., processLogger.error or
logger.error), and rethrow or propagate any unexpected errors. Specifically,
inside the try/catch that evaluates CEL for a rule (referencing ruleId), detect
CEL-specific failures (e.g., by checking error.name or instanceof
CELRuntimeError/EvaluationError) and log those with structured messages; for all
other exceptions, rethrow so they are not silently swallowed. Also ensure the
CEL evaluation is awaited (do not void a promise) so these errors are properly
caught.
- Around line 57-60: The catch-all in the logging path that currently does
console.error('Failed to log sign-up rule trigger:', e) must be removed or
narrowed so we don't swallow errors; update the code around the logging call
that emits "Failed to log sign-up rule trigger" to either remove the try/catch
and let the caller handle the promise rejection, or catch only expected error
types and rethrow after logging; replace console.error with the project's
structured logger (e.g., logger.error) and include the error object, ensuring
any unexpected errors are propagated instead of silently ignored.
In `@apps/dashboard/src/components/rule-builder/condition-builder.tsx`:
- Around line 65-72: The handler handleFieldChange currently resets value to an
empty string for all operator cases which corrupts the expected shape for
operators that expect arrays; change the reset logic so that after computing
operator (using getOperatorsForField and condition.operator) you set value to an
empty array when operator === 'in_list' and to an empty string otherwise, then
call onChange({...condition, field, operator, value}) so in_list values remain
arrays.
🧹 Nitpick comments (5)
packages/stack-shared/src/interface/crud/users.ts (2)
23-42: Consider extracting the validation test to a reusable helper.The validation test for
restricted_by_admin_consistencyis duplicated verbatim in both the update schema (here) and the read schema (lines 87-103). Extract it into a shared function to follow DRY principles.Also, per coding guidelines, the
anytype on line 29 should have a comment explaining why it's necessary.♻️ Proposed refactor
Add a shared helper before the schemas:
function restrictedByAdminConsistencyTest(this: yup.TestContext<unknown>, value: unknown): boolean | yup.ValidationError { // yup test callbacks receive untyped values; we validate shape at runtime if (value == null || typeof value !== 'object') return true; const v = value as Record<string, unknown>; if (v.restricted_by_admin !== true) { if (v.restricted_by_admin_reason != null) { return this.createError({ message: "restricted_by_admin_reason must be null when restricted_by_admin is not true" }); } if (v.restricted_by_admin_private_details != null) { return this.createError({ message: "restricted_by_admin_private_details must be null when restricted_by_admin is not true" }); } } return true; }Then use it in both schemas:
-}).defined().test( - "restricted_by_admin_consistency", - "When restricted_by_admin is not true, reason and private_details must be null", - function(this: yup.TestContext<any>, value: any) { - if (value == null) return true; - // If restricted_by_admin is false or missing, both reason and private_details must be null - if (value.restricted_by_admin !== true) { - if (value.restricted_by_admin_reason != null) { - return this.createError({ message: "restricted_by_admin_reason must be null when restricted_by_admin is not true" }); - } - if (value.restricted_by_admin_private_details != null) { - return this.createError({ message: "restricted_by_admin_private_details must be null when restricted_by_admin is not true" }); - } - } - return true; - } -); +}).defined().test( + "restricted_by_admin_consistency", + "When restricted_by_admin is not true, reason and private_details must be null", + restrictedByAdminConsistencyTest +);
87-103: Duplicate validation test — see earlier comment.This test is identical to lines 26-42. Once the shared helper is extracted, apply it here as well.
apps/dashboard/src/components/rule-builder/condition-builder.tsx (1)
45-63: Prefer aMapfor predefined field values.Using a
Recordhere conflicts with the Map preference in the guidelines and makes the lookup less explicit.♻️ Suggested refactor
-const PREDEFINED_VALUES: Partial<Record<ConditionField, string[]>> = { - authMethod: ['password', 'otp', 'oauth', 'passkey'], - oauthProvider: Array.from(standardProviders), -}; +const PREDEFINED_VALUES = new Map<ConditionField, string[]>([ + ['authMethod', ['password', 'otp', 'oauth', 'passkey']], + ['oauthProvider', Array.from(standardProviders)], +]);- const predefinedValues = PREDEFINED_VALUES[condition.field]; + const predefinedValues = PREDEFINED_VALUES.get(condition.field);As per coding guidelines "Use ES6 maps instead of records wherever possible".
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
653-656: Add comment explaining theanycast.The
anycast is used to access internal methods viastackAppInternalsSymbol. Per coding guidelines, add a brief comment explaining why the type system fails here.- const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', { + // eslint-disable-next-line `@typescript-eslint/no-unsafe-member-access` -- stackAppInternalsSymbol is not exposed in public types + const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', {apps/backend/src/lib/users.tsx (1)
85-87: Consider adding a brief comment for the type assertion.While the comment at line 86 explains the purpose, the
as Record<string, unknown>assertion could benefit from a note about why it's safe here.// Merge sign-up rule data into createOrUpdate - // Use type assertion as we know the structure from UsersCrud + // Type assertion is safe: KeyIntersect guarantees these optional metadata fields exist on the intersection type const createOrUpdateWithMeta = createOrUpdate as Record<string, unknown>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/backend/src/lib/users.tsx`:
- Around line 44-72: evaluateSignUpRules may return shouldAllow=false which is
not handled; before calling createOrUpgradeAnonymousUserWithoutRules you must
check ruleResult.shouldAllow and reject the operation when it's false (e.g.,
throw an appropriate error or return a structured rejection) using the sign-up
rule id for context (ruleResult.restrictedBecauseOfSignUpRuleId) so callers can
distinguish a rejection by rule; add this check immediately after obtaining
ruleResult and only proceed to createOrUpgradeAnonymousUserWithoutRules when
ruleResult.shouldAllow is true.
🧹 Nitpick comments (4)
apps/backend/src/lib/cel-evaluator.ts (1)
172-177: Consider defensive assertion for email domain extraction.After the
email.includes('@')check,split('@').pop()should always return a non-empty string for valid emails. However, edge cases like"@domain.com"would pass the check but produce an empty username.The current fallback to
''is acceptable given thatnormalizeEmail(fromemails.tsx) validates email format upstream, but a defensive assertion could make the assumption explicit.Optional: More defensive version
if (params.email) { // Normalize email to match how it's stored in the database email = normalizeEmail(params.email); // Extract domain from normalized email - emailDomain = email.includes('@') ? (email.split('@').pop() ?? '') : ''; + const parts = email.split('@'); + // normalizeEmail validates format, so @ must exist with non-empty parts + emailDomain = parts.length === 2 ? parts[1] : ''; }apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts (1)
1429-1438: Minor: Static email could cause test isolation issues.The test uses a hardcoded email
[email protected]without randomization. If tests run in parallel or leave data behind, this could cause flakiness.💡 Suggested fix
// Test spam domain const spamResponse = await niceBackendFetch("/api/v1/auth/password/sign-up", { method: "POST", accessType: "client", body: { - email: `[email protected]`, + email: `user-${generateSecureRandomString(4)}@spam.com`, password: generateSecureRandomString(), }, });apps/dashboard/src/lib/cel-visual-parser.ts (1)
343-347: Minor: Quote matching in list items could accept mismatched quotes.The regex
^["']((?:\\.|[^"\\])*)["']$would technically match strings with mismatched quotes like"test'. Since this parser consumes CEL generated by the same module (which consistently uses double quotes), this is unlikely to cause issues in practice.💡 Stricter quote matching (optional)
.map(s => { - // Remove surrounding quotes - const match = s.match(/^["']((?:\\.|[^"\\])*)["']$/); - return match ? unescapeCelString(match[1]) : s; + // Remove surrounding double quotes (CEL standard) + const doubleMatch = s.match(/^"((?:\\.|[^"\\])*)"$/); + if (doubleMatch) return unescapeCelString(doubleMatch[1]); + // Fallback for single quotes + const singleMatch = s.match(/^'((?:\\.|[^'\\])*)'$/); + if (singleMatch) return unescapeCelString(singleMatch[1]); + return s; });packages/stack-shared/src/utils/types.tsx (1)
66-69: Add type-level assertions to lock in KeyIntersect's required/optional semantics.The intersection of three mapped types makes shared keys required if required in either
TorU, and can narrow incompatible property types tonever. Add type assertions to document and prevent silent regressions of this behavior.✅ Suggested type assertions
/** * Returns a type whose keys are the intersection of the keys of T and U, deeply. + * Requiredness: a key is required if it is required in either T or U. */ export type KeyIntersect<T, U> = & { [K in keyof T & keyof U]?: T[K] & U[K] } & { [K in RequiredKeys<T> & keyof U]: T[K] & U[K] } & { [K in RequiredKeys<U> & keyof T]: U[K] & T[K] } + +typeAssertIs<KeyIntersect<{ a: string }, { a: string }>, { a: string }>()(); +typeAssertIs<KeyIntersect<{ a?: string }, { a?: string }>, { a?: string }>()();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts`:
- Around line 311-362: The test’s priority values contradict the stated
ordering: either make the behavior consistent by swapping the priorities in the
Project.updateConfig entries so that 'first-allow' has a higher priority number
than 'second-reject' (set 'first-allow'.priority = 1 and
'second-reject'.priority = 0) if your system treats higher numbers as higher
priority, or if your system treats lower numbers as higher priority, update the
earlier comment that describes priority ordering to state "lower number = higher
priority" so the test expectations match the documented behavior.
- Around line 263-265: The inline snapshot in the test assertions
(expect(response).toMatchInlineSnapshot("TODO")) is a placeholder and must be
replaced with the real snapshot: run the test suite (or the single test in
apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts) to
auto-generate the expected snapshot for the response object, then update the
expect(response).toMatchInlineSnapshot call with the generated inline snapshot
content so the assertion matches the actual response.
- Around line 223-265: The test's inline comment in the "should evaluate rules
by priority order (higher number = higher priority)" case is incorrect; update
the comment to read "lower number = higher priority" (or similar) so it matches
the backend implementation documented in sign-up-rules.ts and the test
expectation (priority 0 wins over priority 1) — locate the test case with the
description "should evaluate rules by priority order (higher number = higher
priority)" and edit the comment text accordingly.
🧹 Nitpick comments (2)
apps/backend/src/lib/cel-evaluator.ts (2)
49-51: Incomplete CEL string escape handling.The
unescapeCelStringfunction only handles\\and\"escapes. CEL string literals also support other escape sequences like\n,\t,\r,\b,\f, and Unicode escapes (\uXXXX). If rules use these in patterns (e.g.,email.contains("\n")), they won't be unescaped correctly.♻️ Suggested enhancement
function unescapeCelString(escaped: string): string { - return escaped.replace(/\\\\/g, '\\').replace(/\\"/g, '"'); + return escaped + .replace(/\\n/g, '\n') + .replace(/\\t/g, '\t') + .replace(/\\r/g, '\r') + .replace(/\\b/g, '\b') + .replace(/\\f/g, '\f') + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\'); }Note: Order matters -
\\\\should be last to avoid double-unescaping.
184-253: Consider usingtoMatchInlineSnapshotfor unit tests.Per coding guidelines,
toMatchInlineSnapshotis preferred over other selectors. While the current.toEqual()and.toBe()assertions are clear and correct, switching to inline snapshots would provide better consistency and easier test updates.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/stack-shared/src/known-errors.tsx (1)
1077-1084:⚠️ Potential issue | 🟡 MinorClarify messaging for admin-restricted users.
For
restricted_by_administrator, the current text suggests completing onboarding, which doesn’t apply. Consider a conditional message (or more neutral wording) so admin-restricted users aren’t misdirected.💡 Example adjustment
const TeamInvitationRestrictedUserNotAllowed = createKnownErrorConstructor( KnownError, "TEAM_INVITATION_RESTRICTED_USER_NOT_ALLOWED", (restrictedReason: { type: "anonymous" | "email_not_verified" | "restricted_by_administrator" }) => [ 403, - `Restricted users cannot accept team invitations. Reason: ${restrictedReason.type}. Please complete the onboarding process before accepting team invitations.`, + restrictedReason.type === "restricted_by_administrator" + ? "Restricted users cannot accept team invitations because an administrator restricted this account. Please contact your administrator." + : `Restricted users cannot accept team invitations. Reason: ${restrictedReason.type}. Please complete the onboarding process before accepting team invitations.`, { restricted_reason: restrictedReason, }, ] as const,
🧹 Nitpick comments (4)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
957-979: Validaterestricted_reasonbefore casting.The raw cast can hide unexpected API values and silently misclassify restrictions. Add a defensive guard and throw a clear error when the payload is missing or has an unknown type.
Suggested defensive guard
return { affectedUsers: result.affected_users.map(u => ({ id: u.id, displayName: u.display_name, primaryEmail: u.primary_email, - restrictedReason: u.restricted_reason as { type: "anonymous" | "email_not_verified" | "restricted_by_administrator" }, + restrictedReason: (() => { + const restrictedReason = u.restricted_reason ?? throwErr("restricted_reason missing"); + if ( + restrictedReason.type !== "anonymous" && + restrictedReason.type !== "email_not_verified" && + restrictedReason.type !== "restricted_by_administrator" + ) { + throwErr(`Unexpected restricted_reason.type: ${restrictedReason.type}`); + } + return restrictedReason as { + type: "anonymous" | "email_not_verified" | "restricted_by_administrator" + }; + })(), })), totalAffectedCount: result.total_affected_count, };As per coding guidelines, "Never silently use fallback values when type errors occur. Update types or throw errors instead. Use ?? throwErr(...) over non-null assertions with clear error messages explaining the assumption".
apps/backend/prisma/schema.prisma (1)
1062-1076: Use a Prisma enum forSignupRuleTrigger.actionto enforce valid values at the database layer.Prisma enums with
@mapfor PostgreSQL create native database enums that enforce constraints at the database level, preventing invalid values even if code bypasses the ORM. This improves analytics consistency and type safety.♻️ Proposed refactor
+enum SignupRuleAction { + ALLOW `@map`("allow") + REJECT `@map`("reject") + RESTRICT `@map`("restrict") + LOG `@map`("log") + ADD_METADATA `@map`("add_metadata") +} + // Signup Rules Analytics model SignupRuleTrigger { id String `@default`(uuid()) `@db.Uuid` tenancyId String `@db.Uuid` ruleId String userId String? `@db.Uuid` // User ID if created, null if rejected - action String // allow, reject, restrict, log, add_metadata + action SignupRuleAction metadata Json? // matched conditions, IP, country, etc.packages/stack-shared/src/known-errors.tsx (1)
719-730: Avoidanyand make missing details explicit forSIGN_UP_REJECTED.
constructorArgsFromJsoncurrently acceptsanyand silently falls back to the default message whendetails.messageis missing. Prefer a typed details object and?? throwErr(...)so missing data is explicit and consistent with the guidelines.♻️ Suggested change
const SignUpRejected = createKnownErrorConstructor( KnownError, "SIGN_UP_REJECTED", - (message?: string) => [ - 403, - message ?? "Your sign up was rejected. Please contact us for more information.", - { - message: message ?? "Your sign up was rejected. Please contact us for more information.", - }, - ] as const, - (json: any) => [json.message] as const, + (message: string = "Your sign up was rejected. Please contact us for more information.") => [ + 403, + message, + { message }, + ] as const, + (json: { message?: string } | undefined) => [ + json?.message ?? throwErr("message not found in SignUpRejected details"), + ] as const, );apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
500-505: Avoidanywhen accessingstackAppInternalsSymbol.Line 501: casting to
anybypasses type safety; please add a narrow type (or document whyanyis unavoidable).♻️ Suggested fix
+type StackAdminAppWithInternals = ReturnType<typeof useAdminApp> & { + [stackAppInternalsSymbol]: { + sendRequest: (path: string, init: RequestInit, auth: 'admin') => Promise<Response>; + }; +}; + const fetchAnalytics = async () => { - const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest( + const response = await (stackAdminApp as StackAdminAppWithInternals)[stackAppInternalsSymbol].sendRequest( '/internal/sign-up-rules', { method: 'GET' }, 'admin' // Required for internal endpoints );Based on learnings: “Avoid the
anytype. When necessary, leave a comment explaining why it's being used and why the type system fails at that point.”
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/e2e/tests/backend/endpoints/api/v1/users.test.ts`:
- Around line 675-678: The failing snapshot exposes the private field
restricted_by_admin_private_details in a client access response; remove this
field from the client-side output in the test and ensure the server
serialization logic that produces client responses (the code path that handles
accessType "client" / the user response serializer) omits
restricted_by_admin_private_details for non-admin/client-facing responses;
update the snapshot/assertion to match the sanitized response (keep
restricted_by_admin, restricted_by_admin_reason, restricted_reason as needed)
and add or adjust a server/admin test to assert the private field is present
only for admin/server access if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/internal/sign-up-rules-stats/route.tsx`:
- Around line 55-74: The ClickHouse call using client.query in route.tsx can
throw and currently lacks error handling; wrap the client.query invocation (the
block that assigns to result) in a try-catch or use the same Result.fromPromise
pattern used elsewhere (e.g., analytics query route) to capture failures and
return a structured error response; ensure you handle and log the error, and
convert failures into an appropriate HTTP response instead of letting the
exception bubble.
In `@apps/backend/src/lib/events.tsx`:
- Around line 132-145: The SignUpRuleTriggerEventType schema is missing the
emailDomain field which logRuleTrigger in sign-up-rules.ts includes in event
data; update SignUpRuleTriggerEventType.dataSchema to add an emailDomain entry
(same nullable string semantics as email, e.g., a nullable/defined string) so
the validator accepts the value and the event data isn't dropped or rejected.
In `@apps/backend/src/lib/sign-up-rules.ts`:
- Around line 63-64: The loop over typedEntries(config.auth.signUpRules) uses
object insertion order, not the documented priority order; change to collect
entries into an array and sort them by rule.priority (descending for highest
first) then by ruleId (alphabetical) before iterating, while still skipping
disabled or missing-condition rules (rule.enabled, rule.condition). Also replace
any non-null assertions when reading required fields with a defensive nullish
coalescing that throws a clear error (use your throwErr(...) helper or throw new
Error(...)) so missing priority/condition values fail with a descriptive
message.
🧹 Nitpick comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
489-494: Add a comment explaining theanytype cast.Per coding guidelines, when using
any, leave a comment explaining why it's necessary and why the type system fails at that point.Suggested fix
+ // Type assertion required: stackAppInternalsSymbol is a private symbol not exposed in public types const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest( '/internal/sign-up-rules-stats', { method: 'GET' }, 'admin' // Required for internal endpoints );apps/backend/src/lib/sign-up-rules.ts (1)
20-35: Error handling improved but still swallows errors.The try-catch now uses
captureErrorinstead ofconsole.error, which is an improvement for observability. However, swallowing all errors in the logging path means signup analytics could silently fail without the caller knowing. Consider whether this is the desired behavior or if certain error types should propagate.apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-stats.test.ts (1)
164-170: Potential time calculation fragility in hour verification.The expected hour calculation
new Date(new Date().getTime() - (hourlyCounts.length - 1 - i) * 60 * 60 * 1000)creates a newDate()at assertion time, which could differ from the backend'snowif the test takes more than a few seconds. Consider capturing the timestamp once before the assertion loop.🧩 Suggested improvement
const hourlyCounts = response.body.rule_triggers[0].hourly_counts; expect(hourlyCounts.length).toBe(48); + const assertionTime = new Date(); + assertionTime.setUTCMinutes(0, 0, 0); for (let i = 0; i < hourlyCounts.length - 1; i++) { - expect(hourlyCounts[i].hour).toEqual(new Date(new Date().getTime() - (hourlyCounts.length - 1 - i) * 60 * 60 * 1000).toISOString().slice(0, 13) + ':00:00.000Z'); + const expectedHour = new Date(assertionTime.getTime() - (hourlyCounts.length - 1 - i) * 60 * 60 * 1000); + expect(hourlyCounts[i].hour).toEqual(expectedHour.toISOString().slice(0, 13) + ':00:00.000Z'); expect(hourlyCounts[i].count).toBe(0); } const lastHourlyCount = hourlyCounts[hourlyCounts.length - 1]; - expect(lastHourlyCount.hour).toEqual(new Date().toISOString().slice(0, 13) + ':00:00.000Z'); + expect(lastHourlyCount.hour).toEqual(assertionTime.toISOString().slice(0, 13) + ':00:00.000Z');
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx:
- Around line 563-567: serverRules calculation can throw when
configWithRules.auth.signUpRules is undefined and doesn't reflect evaluation
order; guard against undefined and sort by priority before mapping. Update the
useMemo that computes serverRules to pass a safe object (e.g., default to {}
when signUpRules is falsy) into typedEntries, then sort the resulting array by
rule.priority (use a numeric fallback like 0) in the correct evaluation order
before mapping to { id, rule }, keeping the same dependency on
configWithRules.auth.signUpRules; reference symbols: serverRules, typedEntries,
configWithRules.auth.signUpRules, signUpRules.
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
508-513: Avoidas anywhen accessing internals.
Please add a narrow type (or at least a short rationale comment) instead ofas anyto keep type safety intact.♻️ Possible typing
+type StackAdminAppInternals = { + [stackAppInternalsSymbol]: { + sendRequest: (path: string, init: RequestInit, scope: 'admin') => Promise<Response>; + }; +}; + const fetchAnalytics = async () => { - const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest( + const response = await (stackAdminApp as StackAdminAppInternals)[stackAppInternalsSymbol].sendRequest( '/internal/sign-up-rules-stats', { method: 'GET' }, 'admin' );Based on learnings: Applies to **/*.{ts,tsx} : Avoid the
anytype. When necessary, leave a comment explaining why it's being used and why the type system fails at that point.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
Note
High Risk
Changes core sign-up and user creation/upgrade flows (password/OTP/OAuth) and introduces new DB constraints and user restriction semantics that can block registrations. Also adds CEL expression evaluation and ClickHouse dependencies for logging/stats, increasing the chance of misconfiguration or runtime failures impacting onboarding.
Overview
Adds a sign-up rules enforcement path across password, OTP, and OAuth sign-ups by evaluating CEL-based conditions and either allowing, rejecting (
KnownErrors.SignUpRejected), or marking newly-created/upgraded users as admin-restricted.Introduces admin restriction fields on
ProjectUser(restrictedByAdmin, public reason, private details) with DB constraints, surfaces them through the Users CRUD APIs, and extendscomputeRestrictedStatusto treat admin-restricted users as restricted (with a newrestricted_by_administratorreason).Adds observability + admin UI for sign-up rules: a new
$sign-up-rule-triggerevent persisted to ClickHouse, an internal/internal/sign-up-rules-statsendpoint that returns 48h rule trigger counts for sparklines, and a new dashboard page to create/edit/reorder rules via a visual builder; also adds a user-page dialog/banner to view and manage manual restrictions.Written by Cursor Bugbot for commit 7686d2b. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.