Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a CEL-based sign-up rules engine (evaluator, parser, UI), async rule-trigger logging and stats endpoint, admin-imposed user restriction fields with DB migrations and CRUD plumbing, threads sign-up context through auth flows, dashboard UIs for rule management and user restrictions, ClickHouse/events adjustments, and extensive tests and snapshots. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as User
participant Auth as "Auth Endpoint"
participant RuleEval as "Sign-up Rule Evaluator"
participant CELEngine as "CEL Engine"
participant Events as "Events Logger"
participant Users as "Users CRUD"
participant DB as "Database"
User->>Auth: Sign-up request (email, method, provider)
Auth->>RuleEval: createSignUpRuleContext(context)
RuleEval->>CELEngine: evaluateCelExpression(condition, context)
CELEngine->>CELEngine: preprocess string methods & evaluate
alt rule matches
RuleEval->>Auth: return action (allow/reject/restrict/log)
RuleEval->>Events: async log sign-up-rule-trigger
else no match
RuleEval-->>Auth: default action (allow/reject)
end
Auth->>Users: createOrUpgradeAnonymousUserWithRules(..., signUpRuleOptions)
Users->>DB: persist user (restricted_by_admin* fields)
DB-->>Users: OK
Users-->>Auth: user object
Auth-->>User: response
sequenceDiagram
autonumber
actor Admin as Admin
participant UI as "Dashboard UI"
participant API as "Admin API"
participant DB as "Database"
Admin->>UI: Open user → RestrictionDialog
UI->>API: PATCH /users/{id} (restricted_by_admin, reason, private_details)
API->>DB: Update ProjectUser fields
DB-->>API: OK
API-->>UI: Updated user
UI->>UI: Show RestrictionBanner / RestrictedStatus
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 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.
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
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.
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.
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.
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.
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 == "test@example.com"'); - expect(result).toBeDefined(); - if (result?.type === 'condition') { - expect(result.field).toBe('email'); - expect(result.operator).toBe('equals'); - expect(result.value).toBe('test@example.com'); - } + expect(result).toMatchInlineSnapshot(` + { + "field": "email", + "id": expect.any(String), + "operator": "equals", + "type": "condition", + "value": "test@example.com", + } + `); }); 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.
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.
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.
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.
nams1570
left a comment
There was a problem hiding this comment.
Overall logic is pretty solid, but there are some gaps in behavior I'm curious about. Test coverage looks good.
apps/backend/prisma/migrations/20260201400001_add_restricted_by_admin_constraint/migration.sql
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
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
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.
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.
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
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/backend/src/lib/sign-up-rules.ts`:
- Around line 84-85: The log call is being wrapped in
runAsynchronouslyAndWaitUntil which can detach request-bound dynamic data;
instead remove that wrapper and directly await the logging function
(logRuleTrigger) so it runs in the request context and preserves end-user
info—find the call site where
runAsynchronouslyAndWaitUntil(logRuleTrigger(tenancy, ruleId, context, action))
is used and replace it with a direct awaited call to logRuleTrigger(tenancy,
ruleId, context, action).
In `@apps/backend/src/lib/users.tsx`:
- Around line 61-67: The enrichment object `enrichedCreateOrUpdate` sets
`restricted_by_admin` and `restricted_by_admin_private_details` when
`ruleResult.restrictedBecauseOfSignUpRuleId` is truthy but omits the user-facing
`restricted_by_admin_reason`; update the ternary branch that merges into
`enrichedCreateOrUpdate` to also set `restricted_by_admin_reason` (use a
sensible default like the sign-up rule's public message or a generic "Restricted
by site policy") alongside `restricted_by_admin_private_details`, keeping the
existing logic that composes `existingRestrictionPrivateDetails` and
`restrictionPrivateDetails` and leaving `createOrUpdate` unchanged.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 319-323: The catch block for the save-and-restrict operation
currently only logs the error via captureError/StackAssertionError and leaves
the user uninformed; update the catch in the async handler that calls
setIsSaving and captureError (the block referencing
captureError(`user-restriction-save-and-restrict-error`, new
StackAssertionError(...)) ) to also show a blocking user alert (e.g., call
alert(...) with a clear failure message mentioning the user or action) so the
user is notified of the failure before or after calling captureError; keep the
existing finally that calls setIsSaving(false).
🧹 Nitpick comments (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)
292-294: Add comments explainingas anytype casts.Multiple
as anycasts are used to accessrestrictedByAdminfields (here and at lines 317, 333, 440-442). Per coding guidelines, when usinganyis necessary, leave a comment explaining why the type system fails at that point.♻️ Proposed fix: Add explanatory comment
+ // Type assertion needed: restrictedByAdmin fields are not yet exposed on ServerUser type + // TODO: Update ServerUser type definition to include these fields const restrictedByAdmin = (user as any).restrictedByAdmin ?? false; const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null; const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null;Based on 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/e2e/tests/backend/endpoints/api/v1/restricted-users.test.ts (1)
320-321: Avoidanyin list item mapping.
Use a typed helper (e.g.,{ id: string }) or a narrow cast forlistResponse.body.itemsso the tests stay type-safe.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.Also applies to: 361-362, 392-392, 429-430, 495-496, 542-543
packages/stack-shared/src/known-errors.tsx (1)
719-730: TypeSignUpRejecteddetails instead ofany.
A narrow{ message?: string }keeps deserialization typed without changing behavior.As per coding guidelines: Avoid the `any` type. When necessary, leave a comment explaining why it's being used and why the type system fails at that point.♻️ Suggested fix
- (json: any) => [json.message] as const, + (json: { message?: string }) => [json.message] as const,Also applies to: 1812-1812
apps/dashboard/src/components/rule-builder/condition-builder.tsx (1)
159-169: Empty string filtering may produce unexpected results forin_list.When the user types just whitespace or commas (e.g.,
, , ,), the.filter(Boolean)removes all empty strings, resulting in an empty array[]. However, if the user intentionally clears the input (backspaces everything), the value becomes[]immediately. This is fine, but the UX feedback could be confusing since typing "a, " then deleting "a" leaves[]while typing ", " also leaves[].More importantly, if a user types a single space,
split(',')returns['']→map(trim)returns['']→filter(Boolean)returns[]. This is correct behavior, but consider whether you want to preserve user input display (e.g., show what they typed) separately from the parsed value.apps/backend/src/lib/users.tsx (2)
61-67: Consider simplifying the conditional spread syntax.The
!!ruleResult.restrictedBecauseOfSignUpRuleIdwith spread is functional but could be clearer.♻️ Suggested simplification
const enrichedCreateOrUpdate = { ...createOrUpdate, - ...!!ruleResult.restrictedBecauseOfSignUpRuleId ? { + ...(ruleResult.restrictedBecauseOfSignUpRuleId ? { restricted_by_admin: true, restricted_by_admin_private_details: existingRestrictionPrivateDetails ? `${existingRestrictionPrivateDetails}\n\n${restrictionPrivateDetails}` : restrictionPrivateDetails, - } : {}, + } : {}), };The
!!is unnecessary since the ternary already handles truthiness, and adding parentheses improves readability.
70-75: Type assertion after enrichment may mask type errors.The
as KeyIntersect<...>assertion on line 73 bypasses TypeScript's ability to verify thatenrichedCreateOrUpdateactually matches the expected type. If the enrichment adds fields not in the original type, this could hide issues.Consider whether the type could be adjusted upstream to accept the restriction fields, avoiding the need for the assertion.
apps/backend/src/app/api/latest/users/crud.tsx (1)
638-643: Consider extracting shared validation logic.The admin restriction validation logic in
onCreate(lines 619-626) andonUpdate(lines 1113-1124) is similar but not identical. TheonUpdateversion is more sophisticated (handles effective state). Consider extracting a helper function to ensure consistency and reduce duplication.♻️ Suggested refactor
function validateRestrictedByAdminFields( restrictedByAdmin: boolean, reason: string | null | undefined, privateDetails: string | null | undefined, ): void { if (!restrictedByAdmin) { if (reason != null) { throw new StatusError(StatusError.BadRequest, "restricted_by_admin_reason requires restricted_by_admin=true"); } if (privateDetails != null) { throw new StatusError(StatusError.BadRequest, "restricted_by_admin_private_details requires restricted_by_admin=true"); } } }Then use in both
onCreateandonUpdate(computing effective values as needed).
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/backend/src/lib/clickhouse.tsx`:
- Around line 5-11: The module eagerly calls getEnvVariable for clickhouseUrl,
clickhouseAdminPassword, clickhouseExternalPassword (and sets HAS_CLICKHOUSE) at
import time which causes throws even when ClickHouse is optional; move or defer
these env reads into the createClickhouseClient factory (or provide non-throwing
defaults) and compute HAS_CLICKHOUSE there so missing vars only error when
createClickhouseClient is invoked; update references to clickhouseUrl,
clickhouseAdminPassword, clickhouseExternalPassword, HAS_CLICKHOUSE, and keep
getEnvVariable usage inside the function that actually needs the values.
There was a problem hiding this comment.
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]/sign-up-rules/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
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.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/backend/src/lib/clickhouse.tsx`:
- Around line 5-11: The createClickhouseClient function currently ignores its
database parameter; update it so the createClient call uses the passed-in
database when provided and falls back to
getEnvVariable("STACK_CLICKHOUSE_DATABASE", "default") otherwise (i.e., set
database: database ?? getEnvVariable(...)). Modify createClickhouseClient (and
keep existing authType logic and getEnvVariable usage for username/password) so
callers can override the DB; alternatively remove the unused database parameter
if overriding is not desired.
There was a problem hiding this comment.
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.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
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 523-536: The fetchAnalytics implementation can throw and never
clears the loading flag; wrap its network/JSON parsing logic inside a
try/catch/finally (or add a finally block) so that setIsLoading(false) is always
called regardless of success or error, and in the catch call setAnalytics(new
Map()) or leave previous state as appropriate and log/handle the error; update
the function named fetchAnalytics (and any callers) to ensure
setIsLoading(false) runs in the finally section and keep references to
setAnalytics and setIsLoading unchanged.
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)
512-516: Avoidanyon stack app internals access.
(stackAdminApp as any)[stackAppInternalsSymbol]drops type safety and bypasses the repo rule againstany. Prefer a narrow internal interface (or add a justification comment ifanyis unavoidable).🔧 Suggested refactor (typed internals)
+ type StackAdminAppInternals = { + sendRequest: (path: string, init: RequestInit, scope: 'admin') => Promise<Response>, + }; - const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest( + const internals = (stackAdminApp as { [stackAppInternalsSymbol]: StackAdminAppInternals })[stackAppInternalsSymbol]; + if (!internals) { + throw new StackAssertionError("Missing stack app internals"); + } + const response = await internals.sendRequest( '/internal/sign-up-rules-stats', { method: 'GET' }, 'admin' // Required for internal endpoints );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
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| const config = tenancy.config; | ||
|
|
||
| let restrictedBecauseOfSignUpRuleId: string | null = null; | ||
| for (const [ruleId, rule] of typedEntries(config.auth.signUpRules)) { |
There was a problem hiding this comment.
Sign-up rules not sorted by priority during evaluation
High Severity
The evaluateSignUpRules function iterates over rules using typedEntries(config.auth.signUpRules), which returns entries in arbitrary object property order rather than sorting by the priority field. The comment on line 37 claims "Rules are evaluated in order of priority (highest first), then alphabetically by ID" but this sorting is never implemented. Users can reorder rules in the UI (which saves priority values), but the backend ignores these priorities entirely, causing rules to potentially match in an unexpected order.
| // Log specific events to ClickHouse | ||
| const clickhouseEventTypes = ['$token-refresh', '$sign-up-rule-trigger']; | ||
| const matchingEventType = eventTypesArray.find(e => clickhouseEventTypes.includes(e.id)); | ||
| if (matchingEventType) { |
There was a problem hiding this comment.
ClickHouse configuration check removed breaks deployments
Medium Severity
The isClickhouseConfigured() check was removed from event logging, but getClickhouseAdminClient() now throws when ClickHouse environment variables are missing (because getEnvVariable without a default value throws "Missing environment variable"). When logging $token-refresh or $sign-up-rule-trigger events in deployments without ClickHouse configured, this will cause errors inside runAsynchronouslyAndWaitUntil. Previously, the check prevented attempting to use ClickHouse when not configured.


Note
High Risk
Touches core sign-up/auth flows and user restriction semantics (including new DB constraints) and introduces dynamic rule evaluation/logging; misconfiguration or CEL/parser bugs could block sign-ups or incorrectly restrict users.
Overview
Introduces CEL-based sign-up rules (config-driven) that are evaluated during password/OTP/OAuth sign-ups and anonymous upgrades; matching rules can reject sign-ups or mark users as admin-restricted, and triggers are logged for analytics.
Extends
ProjectUserwithrestrictedByAdminplus public/private restriction details, updates restriction computation/filtering, and exposes these fields via user CRUD (including validation + DB constraint enforcing consistency when unrestricted).Adds a new dashboard Sign-up Rules page with a visual condition builder (CEL <-> visual tree), drag-reorder by priority, per-rule 48h sparkline analytics via a new hidden internal endpoint, and adds user-page UI to view/edit manual restrictions. Also refactors ClickHouse client initialization to require env vars (removing
isClickhouseConfiguredchecks) and adjusts CI container startup wait time.Written by Cursor Bugbot for commit 2141e68. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
APIs & Schema
Tests
Docs
Chores