Skip to content

fix: Replace real names with auto-generated aliases for privacy (depends on #39)#40

Merged
webdevcody merged 7 commits intowebdevcody:mainfrom
amoscicki:AM02/bugfix/33-user-aliases
Dec 22, 2025
Merged

fix: Replace real names with auto-generated aliases for privacy (depends on #39)#40
webdevcody merged 7 commits intowebdevcody:mainfrom
amoscicki:AM02/bugfix/33-user-aliases

Conversation

@amoscicki
Copy link
Copy Markdown
Contributor

@amoscicki amoscicki commented Dec 21, 2025

Depends on #39 - This PR should be merged after feat: Add shareable dev patches infrastructure

Summary

Adds privacy controls for user profiles by replacing real names with auto-generated aliases and giving users control over which name is displayed publicly.

Fixes #33 - Replace real names with auto-generated aliases in course discussions.

Rationale

User Privacy:

  • Real names (from OAuth) should not be exposed without user consent
  • Users can choose to display either their alias or real name publicly
  • PII (realName, useDisplayName) is stripped from all public API responses

Admin Moderation:

  • Admins can still see both alias and real name for moderation purposes
  • Helps identify users while respecting their public privacy preferences

What's Included

1. Alias Generator (src/utils/alias-generator.ts)

Generates unique human-readable aliases in format: AdjectiveNoun123

  • 500+ adjectives and nouns for variety
  • Hash-based number suffix for uniqueness
  • Collision detection with retry logic

2. Privacy Toggle (useDisplayName)

Setting Public Display
true (default) Shows alias (displayName)
false Shows real name

3. PII Stripping (src/utils/name-helpers.ts)

Function Purpose
getPublicName() Computes which name to show based on setting
toPublicProfile() Strips realName/useDisplayName from responses
addPublicNameForAdmin() Keeps all data for admin views

4. Updated Components

  • Profile edit page: Toggle switch for name preference
  • Public profile: Shows computed publicName
  • Members page: Shows publicName for all users
  • Comments: Uses publicName instead of userName
  • Admin pages: Shows both names for moderation

Test Plan

  • New user signup → alias generated automatically
  • Toggle useDisplayName in profile settings
  • Public profile shows correct name based on setting
  • Network responses don't contain realName on public endpoints
  • Admin comments page shows both alias and real name

Summary by CodeRabbit

  • New Features
    • Added a Real Name field and a "Use Display Name" toggle on profiles.
    • Profile create/edit flows accept and honor real-name preference.
    • Auto-generated privacy-friendly aliases for new accounts to avoid display-name collisions.
    • Added display-name existence check to prevent duplicates.
    • UI now uses a computed public name for avatars, member cards, comments, admin lists, and profile pages; shows real/alias indicators where appropriate.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 21, 2025

Walkthrough

Adds realName and useDisplayName to profiles, generates unique privacy-friendly aliases for Google signups, provides name helper utilities to compute a publicName, updates DB migrations/schema, data-access, use-cases, form validation, and multiple UI components to use publicName with conditional real/alias rendering.

Changes

Cohort / File(s) Summary
Migrations & Schema
drizzle/0047_spicy_randall_flagg.sql, drizzle/0048_use_display_name.sql, drizzle/meta/_journal.json, src/db/schema.ts
Add realName (text) and useDisplayName (boolean, notNull, default true) to profiles; append migration journal entries.
Alias generation utility
src/utils/alias-generator.ts
New module exporting generateAlias() and generateUniqueAlias(checkExists, maxAttempts) with retry and timestamp-fallback to produce readable, unique aliases.
Name helpers
src/utils/name-helpers.ts
New ProfileWithNames type and helpers getPublicName(), toPublicProfile(), and addPublicNameForAdmin() to compute/attach publicName and strip/hide realName/useDisplayName where appropriate.
Profile data access
src/data-access/profiles.ts
createProfile signature extended to accept optional realName; added displayNameExists(displayName: string): Promise<boolean>; selections include realName and useDisplayName; public-profile functions use name helpers.
Comments & Affiliates data access
src/data-access/comments.ts, src/data-access/affiliates.ts
Apply toPublicProfile / addPublicNameForAdmin transformations to comment and affiliate projections; affiliates projection includes userRealName and useDisplayName.
Registration / Use case
src/use-cases/users.ts
createGoogleUserUseCase uses generateUniqueAlias(displayNameExists) to create a privacy-friendly alias as displayName and stores Google name as realName when calling createProfile.
Routes / UI — name rendering
src/routes/admin/affiliates.tsx, src/routes/admin/comments.tsx, src/routes/learn/$slug/-components/comment-form.tsx, src/routes/learn/$slug/-components/comment-list.tsx, src/routes/members.tsx, src/routes/profile/$userId.tsx
Replace direct displayName usages with publicName for avatar seeds, alt text, initials, and displayed names; show real/alias indicators conditionally based on useDisplayName and realName.
Profile edit form & validation
src/routes/profile/edit.tsx, src/fn/profiles.ts
Extend profile form schema and UI to include realName (optional) and useDisplayName toggle; bind inputs via controllers and validate new fields.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Google OAuth)
    participant Reg as createGoogleUserUseCase
    participant Alias as AliasGenerator
    participant ProfilesDA as Profiles Data Access
    participant DB as Database
    participant UI as Comments/Profile UI

    User->>Reg: Sign up via Google (name, picture)
    Reg->>Alias: generateUniqueAlias(checkExists)
    Alias->>ProfilesDA: checkExists(candidate)
    ProfilesDA->>DB: SELECT EXISTS ... ILIKE candidate
    DB-->>ProfilesDA: exists? (true/false)
    ProfilesDA-->>Alias: result
    Alias-->>Reg: unique alias (or timestamp-fallback)
    Reg->>ProfilesDA: createProfile(userId, alias, picture, realName)
    ProfilesDA->>DB: INSERT profile (displayName=alias, realName=..., useDisplayName=true)
    DB-->>ProfilesDA: inserted
    ProfilesDA-->>Reg: created
    Reg-->>User: registration complete

    User->>UI: View comments/profile
    UI->>ProfilesDA: getComments()/getProfile()
    ProfilesDA->>DB: SELECT comments + profiles (includes realName + useDisplayName)
    DB-->>ProfilesDA: rows
    ProfilesDA->>ProfilesDA: toPublicProfile()/addPublicNameForAdmin() → publicName
    ProfilesDA-->>UI: comments/profile with publicName
    UI-->>User: display publicName (alias or real per useDisplayName)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: replacing real names with auto-generated aliases for privacy, which matches the primary change across the PR.
Linked Issues check ✅ Passed All key objectives from issue #33 are met: alias generation with uniqueness checks, registration flow updated, PII stripping for public APIs, admin visibility, user customization via profile settings, and readable alias format (AdjectiveNoun##).
Out of Scope Changes check ✅ Passed All changes are aligned with privacy feature objectives; no unrelated modifications detected. Database schema, utilities, UI components, and use cases all support the alias/privacy system.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2b6f5c and f2c3bf9.

📒 Files selected for processing (1)
  • src/routes/learn/$slug/-components/comment-list.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/routes/learn/$slug/-components/comment-list.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Playwright Tests

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/learn/$slug/-components/comment-form.tsx (1)

125-133: Use computed publicName field for consistency.

This component implements inline logic to determine which name to display, while other components in this PR (e.g., comment-list.tsx, members.tsx) use the computed publicName field. This inconsistency creates maintenance issues—if the name determination logic changes in src/utils/name-helpers.ts, this component won't automatically reflect those changes.

🔎 Recommended refactor to use publicName
                <img
                  className="max-h-10 w-auto object-cover"
                  src={
                    profile?.image ??
-                    `https://api.dicebear.com/9.x/initials/svg?seed=${(profile?.useDisplayName === false && profile?.realName) ? profile.realName : (profile?.displayName || "user")}&backgroundColor=6366f1&textColor=ffffff`
+                    `https://api.dicebear.com/9.x/initials/svg?seed=${profile?.publicName || "user"}&backgroundColor=6366f1&textColor=ffffff`
                  }
                  alt="Your avatar"
                />

This assumes the profile object includes the publicName field. If not, you'll need to update the data-access layer to include it.

🧹 Nitpick comments (11)
drizzle/meta/_journal.json (1)

334-347: Migration entries structurally correct; consider naming consistency.

The two new migration journal entries are well-formed with correct sequential idx values (47, 48) and monotonically increasing timestamps. Migration 0047 follows the established naming convention (0047_spicy_randall_flagg), but migration 0048 uses a descriptive tag name (0048_use_display_name) instead of the pattern followed throughout—<number>_<adjective>_<character_name> (e.g., 0046_sticky_kinsey_walden, 0045_blue_miek).

The migration files exist and contain valid SQL. If naming consistency is preferred, consider regenerating migration 0048 with a pattern-matching name; otherwise, document why certain migrations use descriptive names.

src/utils/alias-generator.ts (1)

490-511: Consider collision probability and fallback format consistency.

The unique alias generation has a minor concern:

With 10 retry attempts and ~4.8M possible combinations, the collision probability is low for small user bases but may increase as the database grows. For a production system with thousands of users, you might want to:

  1. Increase retry attempts to 20-50 as the user base grows
  2. Monitor collision rates and adjust the retry count dynamically

Additionally, the fallback format uses a timestamp suffix (${adjective} ${noun} ${timestamp}) which differs from the primary format (${adjective} ${noun} ${number}). This is acceptable, but consider documenting that the fallback may produce slightly different-looking aliases.

🔍 Estimated collision probability

For reference, with a random selection from 4.8M combinations:

  • At 1,000 users: ~0.01% chance of collision per generation
  • At 10,000 users: ~1% chance per generation
  • At 50,000 users: ~25% chance per generation

The current 10 attempts should handle small-to-medium deployments well.

.dev/patches/dev-login-bypass.patch (2)

621-659: Consider extracting magic values for maintainability.

The devLoginFn function contains several inline strings and patterns:

  • "dev-" prefix for googleId (line 610, 634, 715)
  • DiceBear style list (lines 576-585)
  • Hash function implementation (lines 588-595)

Consider extracting these to constants at the top of the file to improve maintainability and prevent typos.

🔎 Suggested refactor
+// Dev user configuration
+const DEV_USER_PREFIX = "dev-";
+const DEV_USER_ID_PATTERN = /^dev-/;
+
 // DiceBear avatar styles that work reliably
 const DICEBEAR_STYLES = [
   "lorelei",
   ...
 ];

 function createMockGoogleUser(email: string, name: string): GoogleUser {
   ...
   return {
-    sub: `dev-${email.replace(/[^a-z0-9]/gi, "-")}`,
+    sub: `${DEV_USER_PREFIX}${email.replace(/[^a-z0-9]/gi, "-")}`,
     ...
   };
 }

73-395: DevFloatingMenu: Consider accessibility and mobile experience.

The floating dev menu is feature-rich, but has potential UX concerns:

  1. Keyboard accessibility: The draggable functionality may not work with keyboard-only navigation
  2. Mobile experience: Dragging on touch devices may conflict with scrolling
  3. Screen readers: The drag-to-reposition hint may not be announced to screen reader users

Consider adding:

  • Keyboard shortcuts (e.g., arrow keys) for repositioning
  • Touch event handlers for mobile
  • ARIA labels and instructions

Based on learnings: Routes should implement error boundaries and proper accessibility patterns.

src/routes/profile/edit.tsx (1)

420-445: Add accessibility label to the clear button.

The clear button implementation is good, but it lacks an accessible label. Screen reader users won't know what the button does.

Proposed fix
                      {profileForm.watch("realName") && (
                        <Button
                          type="button"
                          variant="outline"
                          size="sm"
+                         aria-label="Clear real name"
                          onClick={() => {
                            profileForm.setValue("realName", "");
                          }}
                        >
                          <X className="h-4 w-4" />
                        </Button>
                      )}
src/data-access/profiles.ts (1)

132-157: Consider using toPublicProfile utility for consistency.

The manual PII stripping here works correctly, but differs from the pattern in getPublicProfile (line 78) which uses the toPublicProfile helper. While the custom select statement may require manual handling, consider whether the returned shape could be made compatible with toPublicProfile for consistency across the codebase.

src/utils/name-helpers.ts (1)

1-5: Consider exporting ProfileWithNames type.

The ProfileWithNames type provides a clear contract for the name helper functions. Exporting it would allow other modules to type-check their profile objects before passing them to these utilities, improving type safety across the codebase.

Proposed change
-type ProfileWithNames = {
+export type ProfileWithNames = {
   displayName: string | null;
   realName: string | null;
   useDisplayName: boolean;
 };
.dev/README.md (1)

31-42: Add language specifier to fenced code block.

The directory structure code block is missing a language identifier, which markdown linters flag. Adding "text" after the opening backticks improves markdown compliance.

Proposed fix
-```
+```text
 .dev/
 ├── README.md              # This file
 ├── setup.sh               # Run once to add git aliases

Based on static analysis hint from markdownlint-cli2.

src/routes/profile/$userId.tsx (1)

67-71: Consider more targeted cache invalidation instead of disabling cache entirely.

Setting staleTime: 0 disables query caching, which forces a fresh fetch on every render. While this ensures the privacy toggle (useDisplayName) updates immediately, it may impact performance if the profile page re-renders frequently.

🔎 Alternative approach using targeted invalidation

Instead of disabling the cache globally, invalidate the query only when the privacy toggle changes:

// In the profile edit component where the toggle is changed:
const queryClient = useQueryClient();

// After updating useDisplayName preference:
queryClient.invalidateQueries({ 
  queryKey: ["profile", "public", userId] 
});

This preserves caching benefits while ensuring updates are reflected when needed.

.dev/dev-patch.sh (2)

84-91: Consider separating declaration and assignment to avoid masking return values.

Line 88 combines local declaration with command substitution: local state=$(get_state "$patch"). If get_state fails, the exit code is masked by the local command.

As per static analysis (shellcheck SC2155), consider:

 status() {
     echo "Dev Patches Status:"
     echo "-------------------"
     for patch in $(get_all_patches); do
-        local state=$(get_state "$patch")
+        local state
+        state=$(get_state "$patch")
         [ "$state" = "on" ] && echo -e "  ${GREEN}$patch: $state${NC}" || echo -e "  $patch: $state"
     done
 }

This allows set -e to catch failures in get_state if they occur.


7-9: Add validation for STATE_FILE existence and format.

The script assumes STATE_FILE exists and is properly formatted, but doesn't validate this. If the file is missing or malformed, the script will fail with cryptic errors.

🔎 Suggested validation at script start

Add validation after the variable declarations:

 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 PATCHES_DIR="$SCRIPT_DIR/patches"
 STATE_FILE="$SCRIPT_DIR/patches.yaml"
+
+# Validate state file exists
+if [ ! -f "$STATE_FILE" ]; then
+    echo -e "${RED}ERROR: State file not found: $STATE_FILE${NC}" >&2
+    echo "Run setup script to initialize dev environment" >&2
+    exit 1
+fi
+
+# Validate patches directory exists
+if [ ! -d "$PATCHES_DIR" ]; then
+    echo -e "${RED}ERROR: Patches directory not found: $PATCHES_DIR${NC}" >&2
+    exit 1
+fi

This provides clearer error messages when the development environment isn't properly initialized.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed15046 and 9fc2d3d.

📒 Files selected for processing (27)
  • .dev/README.md (1 hunks)
  • .dev/dev-patch.sh (1 hunks)
  • .dev/hooks/pre-commit-patch-guard.sh (1 hunks)
  • .dev/patches.yaml (1 hunks)
  • .dev/patches/dev-login-bypass.patch (1 hunks)
  • .dev/patches/mock-storage.patch (1 hunks)
  • .dev/setup.sh (1 hunks)
  • .env.sample (1 hunks)
  • .gitattributes (1 hunks)
  • drizzle/0047_spicy_randall_flagg.sql (1 hunks)
  • drizzle/0048_use_display_name.sql (1 hunks)
  • drizzle/meta/_journal.json (1 hunks)
  • src/data-access/affiliates.ts (1 hunks)
  • src/data-access/comments.ts (4 hunks)
  • src/data-access/profiles.ts (5 hunks)
  • src/db/schema.ts (1 hunks)
  • src/fn/profiles.ts (1 hunks)
  • src/routes/admin/affiliates.tsx (1 hunks)
  • src/routes/admin/comments.tsx (3 hunks)
  • src/routes/learn/$slug/-components/comment-form.tsx (1 hunks)
  • src/routes/learn/$slug/-components/comment-list.tsx (3 hunks)
  • src/routes/members.tsx (2 hunks)
  • src/routes/profile/$userId.tsx (6 hunks)
  • src/routes/profile/edit.tsx (5 hunks)
  • src/use-cases/users.ts (2 hunks)
  • src/utils/alias-generator.ts (1 hunks)
  • src/utils/name-helpers.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: When invoking a TanStack server function, pass an object with a data property containing parameters
All card components should use the shadcn Card component with CardTitle, CardDescription, and other shadcn Card subcomponents
All server-side operations should use TanStack Start server functions created with createServerFn instead of direct backend logic
Form handling should use React Hook Form with Zod validation

**/*.{ts,tsx}: Never hard code magic numbers into code; consolidate them to the top of the file or in /src/config/index.ts to keep the code clean
Never allow a file to exceed 1,000 lines of code; split overly large files into smaller modular components to maintain code maintainability

Files:

  • src/utils/alias-generator.ts
  • src/routes/profile/edit.tsx
  • src/utils/name-helpers.ts
  • src/routes/learn/$slug/-components/comment-form.tsx
  • src/fn/profiles.ts
  • src/routes/admin/comments.tsx
  • src/routes/admin/affiliates.tsx
  • src/data-access/comments.ts
  • src/routes/members.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
  • src/use-cases/users.ts
  • src/data-access/profiles.ts
  • src/routes/profile/$userId.tsx
  • src/db/schema.ts
  • src/data-access/affiliates.ts
**/routes/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

**/routes/**/*.tsx: Pages should use the Page component and PageHeader component when possible
Implement error boundaries at the route level using DefaultCatchBoundary

Files:

  • src/routes/profile/edit.tsx
  • src/routes/learn/$slug/-components/comment-form.tsx
  • src/routes/admin/comments.tsx
  • src/routes/admin/affiliates.tsx
  • src/routes/members.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
  • src/routes/profile/$userId.tsx
**/{components,routes}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Component styling should follow shadcn/ui patterns with Tailwind CSS v4

Files:

  • src/routes/profile/edit.tsx
  • src/routes/learn/$slug/-components/comment-form.tsx
  • src/routes/admin/comments.tsx
  • src/routes/admin/affiliates.tsx
  • src/routes/members.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
  • src/routes/profile/$userId.tsx
**/{fn,functions}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/{fn,functions}/**/*.{ts,tsx}: When making a TanStack server function that requires authentication, use .middleware([authenticatedMiddleware])
When making a TanStack server function that requires admin-only permission, use .middleware([adminMiddleware])
When making a TanStack server function with optional authentication, use .middleware([unauthenticatedMiddleware])

Files:

  • src/fn/profiles.ts
src/fn/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/tanstack-server-functions.mdc)

src/fn/**/*.ts: When creating a TanStack Start server function, always include at least one middleware function call. Use existing middleware functions from src/lib/auth.ts
Always attach a validator to server functions using Zod or similar validation library
Never import Drizzle-related objects inside server function handlers. Instead, invoke use cases based on the layered architecture pattern. Reference docs/technical/layered-architecture.md for architecture details

Files:

  • src/fn/profiles.ts
**/routes/admin/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

When making an admin page, protect it using assertIsAdminFn() in the beforeLoad hook with createFileRoute

Files:

  • src/routes/admin/comments.tsx
  • src/routes/admin/affiliates.tsx
**/data-access/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use the Repository pattern (defined in /src/data-access/) for database query functions with Drizzle ORM

Files:

  • src/data-access/comments.ts
  • src/data-access/profiles.ts
  • src/data-access/affiliates.ts
🧠 Learnings (5)
📚 Learning: 2025-12-19T06:04:43.431Z
Learnt from: CR
Repo: webdevcody/agentic-jumpstart PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T06:04:43.431Z
Learning: Applies to **/*.{ts,tsx} : Form handling should use React Hook Form with Zod validation

Applied to files:

  • src/routes/profile/edit.tsx
📚 Learning: 2025-12-19T06:04:43.431Z
Learnt from: CR
Repo: webdevcody/agentic-jumpstart PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T06:04:43.431Z
Learning: Applies to **/routes/admin/**/*.tsx : When making an admin page, protect it using `assertIsAdminFn()` in the `beforeLoad` hook with `createFileRoute`

Applied to files:

  • .dev/patches/dev-login-bypass.patch
📚 Learning: 2025-12-19T06:04:43.431Z
Learnt from: CR
Repo: webdevcody/agentic-jumpstart PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T06:04:43.431Z
Learning: Applies to **/routes/**/*.tsx : Pages should use the `Page` component and `PageHeader` component when possible

Applied to files:

  • .dev/patches/dev-login-bypass.patch
📚 Learning: 2025-12-19T06:04:43.431Z
Learnt from: CR
Repo: webdevcody/agentic-jumpstart PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T06:04:43.431Z
Learning: Applies to **/routes/**/*.tsx : Implement error boundaries at the route level using `DefaultCatchBoundary`

Applied to files:

  • .dev/patches/dev-login-bypass.patch
📚 Learning: 2025-12-19T06:04:43.431Z
Learnt from: CR
Repo: webdevcody/agentic-jumpstart PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T06:04:43.431Z
Learning: Routes are auto-generated to `/src/routeTree.gen.ts` and should never be edited manually

Applied to files:

  • .dev/patches/dev-login-bypass.patch
🧬 Code graph analysis (8)
src/utils/alias-generator.ts (1)
src/utils/storage/r2.ts (1)
  • exists (65-83)
src/routes/profile/edit.tsx (1)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/data-access/comments.ts (1)
src/utils/name-helpers.ts (2)
  • toPublicProfile (14-22)
  • addPublicNameForAdmin (24-31)
src/routes/members.tsx (1)
src/components/ui/avatar.tsx (1)
  • AvatarFallback (50-50)
src/use-cases/users.ts (2)
src/utils/alias-generator.ts (1)
  • generateUniqueAlias (494-511)
src/data-access/profiles.ts (2)
  • displayNameExists (175-180)
  • createProfile (14-31)
src/data-access/profiles.ts (2)
src/db/schema.ts (1)
  • profiles (41-59)
src/utils/name-helpers.ts (2)
  • toPublicProfile (14-22)
  • getPublicName (7-12)
src/routes/profile/$userId.tsx (2)
src/fn/profiles.ts (1)
  • getPublicProfileFn (43-50)
src/components/ui/avatar.tsx (1)
  • AvatarFallback (50-50)
src/data-access/affiliates.ts (1)
src/db/schema.ts (1)
  • profiles (41-59)
🪛 markdownlint-cli2 (0.18.1)
.dev/README.md

31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Shellcheck (0.11.0)
.dev/dev-patch.sh

[warning] 88-88: Declare and assign separately to avoid masking return values.

(SC2155)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Playwright Tests
🔇 Additional comments (46)
src/db/schema.ts (1)

48-49: LGTM! Well-designed privacy-focused schema additions.

The new realName and useDisplayName fields are well-conceived:

  • realName being nullable allows users to clear their OAuth name for privacy
  • useDisplayName defaulting to true ensures privacy-by-default (shows alias instead of real name)
  • Clear inline comments explain the purpose of each field
src/fn/profiles.ts (1)

20-21: LGTM! Validation schema correctly extended.

The validation schema properly extends the profile update schema with the new privacy fields:

  • realName max length of 100 is reasonable
  • Correctly marked as optional and nullable to match the database schema
  • useDisplayName as optional boolean allows partial updates
.env.sample (1)

34-41: LGTM! Clear documentation for development features.

The new dev patches section is well-documented:

  • Clear separation from production config
  • Helpful comments explain each variable's purpose
  • References .dev/README.md for more details
  • Properly marked as optional and dev-only
.gitattributes (1)

1-32: LGTM! Essential for cross-platform consistency.

This .gitattributes configuration is crucial for the dev patch system:

  • Forces LF line endings globally, preventing Windows CRLF issues
  • Critical for shell scripts (.sh files) which fail on Windows CRLF
  • Comprehensive coverage of text file types (TypeScript, SQL, patches, etc.)
  • Binary files properly marked

This prevents common issues where shell scripts become non-executable due to line ending problems.

src/utils/alias-generator.ts (1)

480-488: LGTM! Simple and effective alias generation.

The basic alias generation is well-implemented:

  • Large word lists (232 adjectives, 231 nouns) provide good variety
  • Two-digit suffix (10-99) adds 90 variations per word combo
  • Total combinations: ~4.8 million (232 × 231 × 90)
  • Format is human-readable and memorable
.dev/patches/mock-storage.patch (3)

794-887: MockStorage implementation is practical for development.

The mock storage implementation is well-designed for dev use:

  • Uses free, publicly available media (Google Cloud videos, Unsplash images)
  • Consistent hashing ensures deterministic media selection
  • In-memory file storage for uploads
  • Proper IStorage interface implementation

However, note that this relies on external services (Google Cloud, Unsplash) being available. If these services are down or rate-limit requests, development could be impacted.

Consider documenting these external dependencies and their potential failure modes in the .dev/README.md.


782-787: LGTM! Improved robustness for thumbnail key generation.

The updated getThumbnailKey function now handles non-.mp4 extensions gracefully by appending _thumb.jpg to any base key format. This is more flexible and prevents edge case failures.


9-21: LGTM! Proper storage type discrimination for mock support.

The changes to support both "r2" and "mock" storage types are consistently applied:

  • getAvailableQualitiesFn now accepts mock storage for presigned URLs
  • getThumbnailUrlFn correctly handles both R2 and mock for thumbnails
  • Hero component's getFirstVideoSegmentFn checks for both storage types

This maintains backward compatibility while enabling the dev workflow.

.dev/patches/dev-login-bypass.patch (4)

753-758: LGTM! Proper route setup with search validation.

The dev-login route correctly:

  • Validates search parameters using TanStack Router patterns
  • Extracts redirect_uri with a sensible default
  • Follows the route pattern conventions

Based on learnings: This follows the expected TanStack Router patterns for the codebase.


14-24: Root loader correctly fetches dev menu config in parallel.

The loader properly uses Promise.all to fetch both shouldShowEarlyAccess and devMenuConfig in parallel, which is efficient. The dev menu is conditionally rendered based on the config.


57-62: Dev login bypass properly integrated into OAuth flow.

The Google login redirect correctly checks DEV_BYPASS_AUTH and redirects to the dev-login page while preserving the original redirect_uri. This maintains the expected OAuth flow structure.


558-659: Verify that dev features are properly guarded in production builds.

The dev auth endpoints include a guard checking process.env.DEV_BYPASS_AUTH === "true", which is in place. However, add NODE_ENV check for defense-in-depth:

  1. The guard currently relies only on the environment variable check. Since Vite automatically sets NODE_ENV=production during build and NODE_ENV=development during dev, adding NODE_ENV !== 'production' as a secondary guard would prevent accidental activation if the environment variable leaks into production.

  2. The patch system includes pre-commit hooks that prevent committing patches to version control, which is good.

  3. The real production risk is operational: ensure your production deployment's .env file does not include DEV_BYPASS_AUTH=true. The build/deployment process should never have this variable set.

Recommended: Add process.env.NODE_ENV !== 'production' check alongside the existing DEV_BYPASS_AUTH guard for additional safety.

drizzle/0047_spicy_randall_flagg.sql (1)

1-1: Migration looks correct, but consider existing user data.

The migration safely adds a nullable realName column to the profiles table. However, consider whether existing users (who registered before this change) need their OAuth names migrated to the realName field.

According to the PR objectives, there's a mention of "migration for existing users whose displayName currently contains real names." You may want to:

  1. Create a follow-up migration to copy existing displayName values to realName for users who haven't opted into aliases yet
  2. Update displayName to auto-generated aliases for those existing users

Do you want me to help generate a data migration script to handle existing users?

drizzle/0048_use_display_name.sql (1)

1-1: LGTM!

The migration correctly adds the useDisplayName column with appropriate defaults. The default value of true aligns with the privacy-first approach (showing aliases by default), and the NOT NULL constraint ensures data integrity.

.dev/hooks/pre-commit-patch-guard.sh (1)

1-13: LGTM!

The pre-commit hook is well-implemented with proper error handling and clear documentation. It correctly prevents commits when dev patches are active, which is important for maintaining code quality.

.dev/patches.yaml (1)

1-6: LGTM!

The patches configuration is clean and well-documented. Both patches are appropriately set to "off" by default, ensuring a safe initial state.

.dev/setup.sh (1)

1-30: LGTM!

The setup script is well-structured with clear output and proper error handling. It provides a good developer experience by setting up convenient git aliases and documenting available commands.

src/routes/learn/$slug/-components/comment-list.tsx (3)

196-196: LGTM! Consistent use of publicName.

The avatar seed correctly uses the computed publicName field with an appropriate fallback. This approach is consistent with the broader naming system introduced in this PR.


206-212: LGTM! Proper display name rendering.

The component correctly uses publicName to display the commenter's name, ensuring consistency with the privacy controls introduced in this PR.


222-244: LGTM! Replied-to user name displayed correctly.

The reply indicator appropriately uses publicName from the replied-to profile, maintaining consistent name display throughout the comment thread.

src/routes/members.tsx (2)

246-263: LGTM! Consistent avatar implementation.

The avatar correctly uses publicName for both the image alt text and fallback initials, with appropriate fallbacks. The initials generation logic is clean and handles edge cases well.


266-273: LGTM! Proper public name display.

The member name display correctly uses publicName with an appropriate fallback to "Anonymous", maintaining consistency with the privacy-focused naming system.

src/data-access/affiliates.ts (1)

227-228: Admin access protection is properly implemented.

The getAllAffiliatesWithStats() function is correctly protected:

  • Server function adminGetAllAffiliatesFn uses adminMiddleware
  • The /admin route enforces assertIsAdminFn() before allowing access
  • PII exposure is prevented by these authorization checks

No action required.

src/routes/profile/edit.tsx (2)

397-418: LGTM! Proper Controller usage for Switch component.

The Controller component is correctly used here for the Switch, as non-native form elements require Controller instead of register with React Hook Form. The implementation provides clear UX with the label and description.


504-514: Good refactoring for consistency.

Converting the isPublicProfile toggle to use Controller makes it consistent with the new useDisplayName toggle and follows React Hook Form best practices for controlled components.

src/data-access/comments.ts (2)

13-36: Excellent PII protection for public API.

The implementation correctly applies toPublicProfile to strip sensitive name fields (realName, useDisplayName) from all profile objects in the comment tree, including nested replies and repliedToProfile. The null-safe handling of repliedToProfile is particularly good defensive programming.


100-112: LGTM! Proper admin view with complete name information.

The use of addPublicNameForAdmin correctly preserves all name fields (realName, useDisplayName, displayName) while adding the computed publicName. This gives admins full visibility for moderation purposes while maintaining the same transformation pattern as the public API.

src/use-cases/users.ts (1)

56-59: Well-implemented privacy enhancement with robust collision handling.

The alias generation correctly uses displayNameExists for collision detection, and per the utility implementation, falls back to timestamp-based suffixes if collisions persist after 10 attempts. This ensures new users always receive a unique alias while prioritizing readable names.

src/routes/admin/comments.tsx (2)

414-442: Name display logic is consistent with admin affiliates page.

The display logic properly shows publicName with conditional hints for real names and aliases. This matches the pattern in src/routes/admin/affiliates.tsx (lines 379-393). Both implementations share the same edge case around useDisplayName !== false potentially matching null/undefined values, though this is unlikely given the schema default of true.


568-591: LGTM! Consistent nested comment display.

The nested comment display correctly mirrors the top-level logic with appropriate size adjustments for the nested context. The implementation ensures all comments, regardless of nesting level, consistently show name information for admin moderation.

src/data-access/profiles.ts (2)

14-30: Clean extension of createProfile function.

The addition of the optional realName parameter is well-positioned and correctly integrated into the insert statement. This enables the privacy-preserving flow where real names are stored separately from display aliases.


65-86: Excellent PII protection in public profile endpoint.

The use of toPublicProfile correctly sanitizes the profile data before returning it, ensuring realName and useDisplayName are never exposed in public API responses. The transformation preserves project data while protecting user privacy.

src/utils/name-helpers.ts (2)

7-12: Excellent privacy-preserving name logic.

The function correctly implements the privacy toggle:

  • When useDisplayName is true → shows alias (displayName)
  • When false AND realName exists → shows realName
  • Falls back to displaying alias if realName is missing
  • Defaults to "Anonymous" if displayName is also missing

This ensures users always have a public-facing name while respecting their privacy preferences.


14-31: Well-designed transformation functions with clear separation.

The two transformation functions serve distinct purposes:

  • toPublicProfile: Removes PII for public API responses
  • addPublicNameForAdmin: Preserves PII for administrative views

Both use generics to maintain type safety while preserving non-name fields. The implementation is clean and the naming clearly indicates the intended use case.

.dev/README.md (1)

1-237: Comprehensive developer documentation.

The patch management system documentation is thorough and well-organized, covering all necessary aspects:

  • Clear setup instructions
  • Command reference
  • Available patches with feature descriptions
  • Troubleshooting guide
  • Patch creation workflow

This will greatly help developers understand and use the patch system effectively.

src/routes/admin/affiliates.tsx (1)

379-393: No changes needed. The useDisplayName field is guaranteed to never be null or undefined in this context.

In the normal user creation flow (createGoogleUserUseCase), a profile is always created immediately after user creation. Additionally, the schema defines useDisplayName: boolean("useDisplayName").notNull().default(true), ensuring the field cannot be null when a profile exists. While getAllAffiliatesWithStats uses a leftJoin, all affiliates will have associated users with existing profiles, so the asymmetric conditions (=== false vs !== false) work as intended without defensive checks for null/undefined values.

src/routes/profile/$userId.tsx (5)

151-157: LGTM! Consistent use of publicName in PageHeader.

The PageHeader correctly uses publicName for the title, highlighted word, and description, ensuring the privacy-respecting name is displayed consistently.


179-184: LGTM! Avatar properly uses publicName and computed initials.

The Avatar component correctly references publicName for alt text and uses the computed initials for the fallback display.


189-199: LGTM! Profile details section uses publicName consistently.

The profile name heading correctly displays publicName, maintaining consistency with the privacy controls throughout the component.


256-264: LGTM! Projects section description references publicName.

The CardDescription properly uses publicName in the user-facing text, maintaining privacy-aware display throughout the projects section.


345-362: LGTM! Empty state message uses publicName.

The empty projects state correctly references publicName instead of generic text, providing a personalized yet privacy-respecting message.

.dev/dev-patch.sh (5)

1-14: LGTM! Standard bash setup with proper error handling.

The script setup follows best practices with set -e for error propagation and portable directory resolution. Color constants are well-defined for user-friendly output.


46-57: LGTM! Robust patch application with 3-way merge fallback.

The function correctly attempts a standard apply first, then falls back to 3-way merge if that fails. Error handling and user feedback are appropriate.


59-68: LGTM! Clean rebuild strategy with proper error propagation.

The rebuild process correctly resets all patches before reapplying those marked as "on", ensuring a consistent state. Error propagation is appropriate.


70-82: LGTM! Well-structured apply and remove operations.

Both functions follow a consistent pattern: check current state, update if needed, rebuild patches. User feedback for no-op cases improves UX.


97-137: LGTM! Comprehensive command dispatcher with clear usage.

The case statement provides a complete set of commands for patch management. The usage help is clear and covers all available operations.

Comment thread .dev/dev-patch.sh
Comment thread .dev/dev-patch.sh
Comment thread src/data-access/profiles.ts
Comment thread src/routes/profile/$userId.tsx Outdated
@amoscicki amoscicki force-pushed the AM02/bugfix/33-user-aliases branch 2 times, most recently from 54239cf to 0c5d402 Compare December 22, 2025 09:19
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/members.tsx (1)

246-262: Fix potential crash in initials computation.

The initials generation logic will fail if publicName contains extra whitespace (leading, trailing, or multiple spaces between words). The expression n[0] returns undefined for empty strings after splitting, causing runtime errors.

🔎 Proposed defensive fix
 <AvatarFallback className="bg-theme-100 dark:bg-theme-900 text-theme-600 dark:text-theme-300 text-2xl font-bold">
   {member.publicName
-    ? member.publicName
-        .split(" ")
-        .map((n) => n[0])
-        .join("")
-        .toUpperCase()
-        .slice(0, 2)
+    ? member.publicName
+        .trim()
+        .split(/\s+/)
+        .filter((n) => n.length > 0)
+        .map((n) => n[0])
+        .join("")
+        .toUpperCase()
+        .slice(0, 2) || "U"
     : "U"}
 </AvatarFallback>

Changes:

  • trim() removes leading/trailing spaces
  • /\s+/ splits on one or more whitespace characters
  • filter((n) => n.length > 0) removes empty segments
  • || "U" provides final fallback
♻️ Duplicate comments (2)
src/data-access/profiles.ts (1)

175-180: Case-insensitive uniqueness check is correct, but database constraint still needed.

The displayNameExists function correctly uses ilike for case-insensitive searching. However, the database-level unique constraint on displayName is still missing, which creates a race condition during concurrent signups (as noted in the previous review comment on this file).

Based on learnings: Past review already identified this issue and provided a solution.

src/routes/profile/$userId.tsx (1)

138-144: Initials computation still vulnerable to empty segments.

This is the same issue flagged in the previous review (see past review comment on lines 138-144). The initials generation will crash if publicName contains extra whitespace. The suggested fix from the previous review should be applied:

  • Use trim() to remove leading/trailing spaces
  • Split on /\s+/ instead of " "
  • Filter out empty segments with filter((n) => n.length > 0)
  • Provide fallback || "U" after slicing
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc2d3d and 0c5d402.

📒 Files selected for processing (19)
  • drizzle/0047_spicy_randall_flagg.sql
  • drizzle/0048_use_display_name.sql
  • drizzle/meta/0047_snapshot.json
  • drizzle/meta/_journal.json
  • src/data-access/affiliates.ts
  • src/data-access/comments.ts
  • src/data-access/profiles.ts
  • src/db/schema.ts
  • src/fn/profiles.ts
  • src/routes/admin/affiliates.tsx
  • src/routes/admin/comments.tsx
  • src/routes/learn/$slug/-components/comment-form.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
  • src/routes/members.tsx
  • src/routes/profile/$userId.tsx
  • src/routes/profile/edit.tsx
  • src/use-cases/users.ts
  • src/utils/alias-generator.ts
  • src/utils/name-helpers.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/routes/admin/comments.tsx
  • drizzle/0047_spicy_randall_flagg.sql
  • src/routes/learn/$slug/-components/comment-list.tsx
  • src/utils/name-helpers.ts
  • src/use-cases/users.ts
  • drizzle/0048_use_display_name.sql
  • src/data-access/comments.ts
  • src/routes/learn/$slug/-components/comment-form.tsx
  • src/fn/profiles.ts
  • src/routes/admin/affiliates.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: When invoking a TanStack server function, pass an object with a data property containing parameters
All card components should use the shadcn Card component with CardTitle, CardDescription, and other shadcn Card subcomponents
All server-side operations should use TanStack Start server functions created with createServerFn instead of direct backend logic
Form handling should use React Hook Form with Zod validation

**/*.{ts,tsx}: Never hard code magic numbers into code; consolidate them to the top of the file or in /src/config/index.ts to keep the code clean
Never allow a file to exceed 1,000 lines of code; split overly large files into smaller modular components to maintain code maintainability

Files:

  • src/db/schema.ts
  • src/routes/members.tsx
  • src/routes/profile/edit.tsx
  • src/data-access/profiles.ts
  • src/utils/alias-generator.ts
  • src/data-access/affiliates.ts
  • src/routes/profile/$userId.tsx
**/routes/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

**/routes/**/*.tsx: Pages should use the Page component and PageHeader component when possible
Implement error boundaries at the route level using DefaultCatchBoundary

Files:

  • src/routes/members.tsx
  • src/routes/profile/edit.tsx
  • src/routes/profile/$userId.tsx
**/{components,routes}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Component styling should follow shadcn/ui patterns with Tailwind CSS v4

Files:

  • src/routes/members.tsx
  • src/routes/profile/edit.tsx
  • src/routes/profile/$userId.tsx
**/data-access/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use the Repository pattern (defined in /src/data-access/) for database query functions with Drizzle ORM

Files:

  • src/data-access/profiles.ts
  • src/data-access/affiliates.ts
🧠 Learnings (1)
📚 Learning: 2025-12-19T06:04:43.431Z
Learnt from: CR
Repo: webdevcody/agentic-jumpstart PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T06:04:43.431Z
Learning: Applies to **/*.{ts,tsx} : Form handling should use React Hook Form with Zod validation

Applied to files:

  • src/routes/profile/edit.tsx
🧬 Code graph analysis (4)
src/routes/members.tsx (1)
src/components/ui/avatar.tsx (1)
  • AvatarFallback (50-50)
src/data-access/profiles.ts (2)
src/db/schema.ts (1)
  • profiles (41-59)
src/utils/name-helpers.ts (2)
  • toPublicProfile (14-22)
  • getPublicName (7-12)
src/utils/alias-generator.ts (1)
src/utils/storage/r2.ts (1)
  • exists (65-83)
src/data-access/affiliates.ts (1)
src/db/schema.ts (1)
  • profiles (41-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Playwright Tests
🔇 Additional comments (14)
drizzle/meta/_journal.json (1)

334-347: LGTM! Migration journal entries are properly formatted.

The two new migration entries (0047 and 0048) for adding realName and useDisplayName fields are correctly formatted and sequenced.

src/data-access/profiles.ts (3)

14-31: LGTM! Backward-compatible extension of createProfile.

The addition of the optional realName parameter is properly implemented and maintains backward compatibility with existing callers.


76-82: LGTM! Proper PII sanitization in public profile.

The use of toPublicProfile correctly strips realName and useDisplayName from public API responses while computing a safe publicName field.


132-157: LGTM! Public members list properly sanitized.

The query correctly fetches realName and useDisplayName for computing publicName, then strips these PII fields from the returned member objects. No privacy leakage.

src/utils/alias-generator.ts (2)

6-470: LGTM! Comprehensive word lists for alias generation.

The extensive adjective (237) and noun (231) lists provide approximately 4.9 million unique combinations when combined with the two-digit number range, offering strong collision resistance for alias generation.


483-488: LGTM! Clean alias generation logic.

The alias format "Adjective Noun ##" is human-readable and the random selection logic is straightforward and correct.

src/db/schema.ts (1)

48-49: LGTM! Privacy fields correctly defined.

The realName and useDisplayName fields are properly defined with appropriate types and helpful comments explaining their privacy purpose. The default value of true for useDisplayName correctly implements privacy-by-default.

Note: The earlier review comment about adding a unique constraint on displayName (line 47) remains relevant for preventing race conditions.

src/routes/members.tsx (1)

267-269: LGTM! Proper fallback for member names.

The fallback chain publicName || "Anonymous" correctly handles missing names for public display.

src/routes/profile/edit.tsx (3)

57-125: LGTM! Form schema and initialization correctly implemented.

The schema extension with realName and useDisplayName follows Zod validation patterns, and the form initialization properly populates default values from the profile data. The use of .or(z.literal("")) for optional URL/string fields is correct.

Based on learnings: Form handling correctly uses React Hook Form with Zod validation.


397-418: LGTM! Display name toggle properly implemented.

The Controller component correctly binds the Switch to the useDisplayName form field, with clear labeling that explains the privacy implications to users.


420-445: LGTM! Real name input with privacy controls.

The real name field includes a clear button for easy privacy management and helpful text explaining when it's required (affiliate program) versus when users may want to clear it (privacy). The implementation correctly uses setValue to reset the field.

src/data-access/affiliates.ts (1)

220-253: LGTM! Affiliate stats correctly extended with name fields.

The addition of userRealName and useDisplayName fields to the projection properly supports the admin UI's need to display both aliases and real names for affiliate management.

src/routes/profile/$userId.tsx (2)

67-72: LGTM! Fresh data fetch for display name toggle.

Setting staleTime: 0 ensures that changes to the useDisplayName toggle are immediately reflected in the profile view. The comment clearly explains this intentional behavior.


138-199: LGTM! Consistent publicName usage for privacy.

The profile page correctly uses publicName throughout for user-facing display elements (title, description, avatar), maintaining privacy by showing the alias instead of real names.

Comment on lines +494 to +511
export async function generateUniqueAlias(
checkExists: (alias: string) => Promise<boolean>,
maxAttempts = 10
): Promise<string> {
for (let i = 0; i < maxAttempts; i++) {
const alias = generateAlias();
const exists = await checkExists(alias);
if (!exists) {
return alias;
}
}

// Fallback: use timestamp-based suffix for guaranteed uniqueness
const adjective = getRandomElement(ADJECTIVES);
const noun = getRandomElement(NOUNS);
const timestamp = Date.now().toString(36).slice(-4);
return `${adjective} ${noun} ${timestamp}`;
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Dec 22, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Timestamp-based fallback does not guarantee uniqueness.

The fallback alias uses Date.now().toString(36).slice(-4) which could produce duplicates if multiple users exhaust retries at the same millisecond. While unlikely, this breaks the uniqueness contract of the function.

Consider using a UUID-based suffix or recursively retrying the timestamp-based alias with the checkExists callback to guarantee uniqueness.

🔎 Proposed fix with UUID-based fallback
 export async function generateUniqueAlias(
   checkExists: (alias: string) => Promise<boolean>,
   maxAttempts = 10
 ): Promise<string> {
   for (let i = 0; i < maxAttempts; i++) {
     const alias = generateAlias();
     const exists = await checkExists(alias);
     if (!exists) {
       return alias;
     }
   }

-  // Fallback: use timestamp-based suffix for guaranteed uniqueness
+  // Fallback: use crypto random for guaranteed uniqueness
   const adjective = getRandomElement(ADJECTIVES);
   const noun = getRandomElement(NOUNS);
-  const timestamp = Date.now().toString(36).slice(-4);
-  return `${adjective} ${noun} ${timestamp}`;
+  const randomSuffix = crypto.randomUUID().slice(0, 4);
+  const fallbackAlias = `${adjective} ${noun} ${randomSuffix}`;
+  
+  // Verify fallback is unique (extremely rare collision)
+  const exists = await checkExists(fallbackAlias);
+  if (!exists) {
+    return fallbackAlias;
+  }
+  
+  // Ultimate fallback with full UUID
+  return `${adjective} ${noun} ${crypto.randomUUID().slice(0, 8)}`;
}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/utils/alias-generator.ts around lines 494 to 511, the timestamp-based
fallback can still collide; replace it with a guaranteed-unique strategy by
generating a UUID-based suffix (or a short random unique token) and/or by
looping until checkExists confirms uniqueness: build the fallback alias (e.g.,
`${adjective} ${noun} ${uuidSuffix}`), call await checkExists(alias) and if it
exists, regenerate the suffix and retry until you find a non-existing alias (or
a reasonable safety limit), then return that verified unique alias.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • Fallback is triggering after 10 attempts
  • What is the probability?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@amoscicki, great question! Let me break down the probability analysis:

Total Search Space

  • Adjectives: ~230 options
  • Nouns: ~250 options
  • Numbers: 90 options (10-99)
  • Total unique aliases: 230 × 250 × 90 = 5,175,000 combinations

Collision Probability

For the fallback to trigger, all 10 attempts must collide with existing aliases. If there are N existing aliases in the database:

P(all 10 attempts fail) ≈ (N / 5,175,000)^10

Here's how this scales with database size:

Existing Users Database Fullness P(Fallback Triggers)
1,000 0.02% ~10^-37 (impossible)
10,000 0.2% ~10^-27 (impossible)
100,000 2% ~10^-17 (impossible)
500,000 10% ~4×10^-8 (1 in 25 million)
1,000,000 20% ~10^-6 (1 in 1 million)
2,500,000 48% ~10^-3 (1 in 1,000)

Practical Implications

The fallback is astronomically unlikely to trigger at realistic user counts. It would only become relevant if:

  • You have hundreds of thousands of active users (>500K)
  • The namespace is significantly full (>10%)

Even at 100K users, a user would need to be extraordinarily unlucky to exhaust 10 attempts. The fallback is effectively a "safety net for the safety net" — theoretically useful but practically never reached at the scale this application is likely to see.

My earlier concern about timestamp collisions is valid in principle, but given how rarely the fallback will trigger, it's more of a theoretical edge case than a practical risk.

- Add alias-generator utility that creates unique human-readable aliases
- Add useDisplayName toggle to profiles schema
- Update user creation to generate aliases instead of using real names
- Use toPublicProfile() to strip realName/useDisplayName from public queries
- Update comment components to use publicName instead of userName
- Update members page to display publicName
- Update public profile page to use safe profile data
- Admin comments page shows both alias and real name
- Admin affiliates page shows publicName
- Keep full profile data for admin moderation purposes
@amoscicki amoscicki force-pushed the AM02/bugfix/33-user-aliases branch from 0c5d402 to 0378435 Compare December 22, 2025 09:26
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/utils/alias-generator.ts (1)

490-511: Critical: Timestamp-based fallback does not guarantee uniqueness.

A past review comment already identified this issue: the fallback alias at lines 506-510 uses Date.now().toString(36).slice(-4), which can produce duplicate aliases if multiple users exhaust the 10 retry attempts at the same millisecond. While the probability is low, this violates the uniqueness guarantee that the function name promises.

Based on learnings, this violates the core requirement from issue #33 for unique alias generation with collision detection.

🔎 Recommended fix: Use crypto-based randomness and verify uniqueness
 export async function generateUniqueAlias(
   checkExists: (alias: string) => Promise<boolean>,
   maxAttempts = 10
 ): Promise<string> {
   for (let i = 0; i < maxAttempts; i++) {
     const alias = generateAlias();
     const exists = await checkExists(alias);
     if (!exists) {
       return alias;
     }
   }

-  // Fallback: use timestamp-based suffix for guaranteed uniqueness
+  // Fallback: use crypto random suffix and verify uniqueness
   const adjective = getRandomElement(ADJECTIVES);
   const noun = getRandomElement(NOUNS);
-  const timestamp = Date.now().toString(36).slice(-4);
-  return `${adjective} ${noun} ${timestamp}`;
+  const randomSuffix = crypto.randomUUID().slice(0, 4);
+  const fallbackAlias = `${adjective} ${noun} ${randomSuffix}`;
+  
+  // Verify fallback is unique (extremely rare collision)
+  const exists = await checkExists(fallbackAlias);
+  if (!exists) {
+    return fallbackAlias;
+  }
+  
+  // Ultimate fallback with full UUID segment
+  return `${adjective} ${noun} ${crypto.randomUUID().slice(0, 8)}`;
}
🧹 Nitpick comments (1)
src/routes/profile/edit.tsx (1)

59-60: Schema asymmetry: client uses empty string while server uses null.

The client form schema uses .or(z.literal("")) for realName, while the server schema (src/fn/profiles.ts:20) uses .nullable(). This creates an asymmetry:

  • Client form: accepts undefined, empty string "", or string
  • Server: accepts undefined, null, or string

When the form submits realName as "", the server stores it as "" rather than null. While this works, it may create semantic confusion between "never set" (null) and "cleared" (empty string).

💡 Consider aligning client and server schemas

Option 1: Use .nullable() on the client to match the server:

-  realName: z.string().max(100).optional().or(z.literal("")),
+  realName: z.string().max(100).optional().nullable(),

Then update the clear button to set null:

-  profileForm.setValue("realName", "");
+  profileForm.setValue("realName", null);

Option 2: Preprocess empty strings to null before server submission in the onProfileSubmit handler.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5d402 and 0378435.

📒 Files selected for processing (19)
  • drizzle/0047_spicy_randall_flagg.sql
  • drizzle/0048_use_display_name.sql
  • drizzle/meta/0047_snapshot.json
  • drizzle/meta/_journal.json
  • src/data-access/affiliates.ts
  • src/data-access/comments.ts
  • src/data-access/profiles.ts
  • src/db/schema.ts
  • src/fn/profiles.ts
  • src/routes/admin/affiliates.tsx
  • src/routes/admin/comments.tsx
  • src/routes/learn/$slug/-components/comment-form.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
  • src/routes/members.tsx
  • src/routes/profile/$userId.tsx
  • src/routes/profile/edit.tsx
  • src/use-cases/users.ts
  • src/utils/alias-generator.ts
  • src/utils/name-helpers.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/routes/learn/$slug/-components/comment-list.tsx
  • drizzle/meta/_journal.json
  • drizzle/0047_spicy_randall_flagg.sql
  • src/routes/admin/affiliates.tsx
  • src/db/schema.ts
  • src/routes/members.tsx
  • src/utils/name-helpers.ts
  • drizzle/0048_use_display_name.sql
  • src/routes/profile/$userId.tsx
  • src/routes/admin/comments.tsx
  • src/use-cases/users.ts
  • src/routes/learn/$slug/-components/comment-form.tsx
  • src/data-access/profiles.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: When invoking a TanStack server function, pass an object with a data property containing parameters
All card components should use the shadcn Card component with CardTitle, CardDescription, and other shadcn Card subcomponents
All server-side operations should use TanStack Start server functions created with createServerFn instead of direct backend logic
Form handling should use React Hook Form with Zod validation

**/*.{ts,tsx}: Never hard code magic numbers into code; consolidate them to the top of the file or in /src/config/index.ts to keep the code clean
Never allow a file to exceed 1,000 lines of code; split overly large files into smaller modular components to maintain code maintainability

Files:

  • src/fn/profiles.ts
  • src/data-access/comments.ts
  • src/data-access/affiliates.ts
  • src/routes/profile/edit.tsx
  • src/utils/alias-generator.ts
**/{fn,functions}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/{fn,functions}/**/*.{ts,tsx}: When making a TanStack server function that requires authentication, use .middleware([authenticatedMiddleware])
When making a TanStack server function that requires admin-only permission, use .middleware([adminMiddleware])
When making a TanStack server function with optional authentication, use .middleware([unauthenticatedMiddleware])

Files:

  • src/fn/profiles.ts
src/fn/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/tanstack-server-functions.mdc)

src/fn/**/*.ts: When creating a tanstack start server function, always include a middleware function call. Existing middleware functions are available in src/lib/auth.ts
When using a server function, always attach a validator to it
When using server functions, never import drizzle related objects inside the function handler. Server functions should always invoke use cases based on the layered architecture pattern. Reference docs/technical/layered-architecture.md for layered architecture guidelines

Files:

  • src/fn/profiles.ts
**/data-access/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use the Repository pattern (defined in /src/data-access/) for database query functions with Drizzle ORM

Files:

  • src/data-access/comments.ts
  • src/data-access/affiliates.ts
**/routes/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

**/routes/**/*.tsx: Pages should use the Page component and PageHeader component when possible
Implement error boundaries at the route level using DefaultCatchBoundary

Files:

  • src/routes/profile/edit.tsx
**/{components,routes}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Component styling should follow shadcn/ui patterns with Tailwind CSS v4

Files:

  • src/routes/profile/edit.tsx
🧠 Learnings (1)
📚 Learning: 2025-12-19T06:04:43.431Z
Learnt from: CR
Repo: webdevcody/agentic-jumpstart PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-19T06:04:43.431Z
Learning: Applies to **/*.{ts,tsx} : Form handling should use React Hook Form with Zod validation

Applied to files:

  • src/routes/profile/edit.tsx
🧬 Code graph analysis (3)
src/data-access/comments.ts (1)
src/utils/name-helpers.ts (2)
  • toPublicProfile (14-22)
  • addPublicNameForAdmin (24-31)
src/data-access/affiliates.ts (1)
src/db/schema.ts (1)
  • profiles (41-59)
src/utils/alias-generator.ts (1)
src/utils/storage/r2.ts (1)
  • exists (65-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Playwright Tests
🔇 Additional comments (9)
src/data-access/comments.ts (3)

4-4: LGTM! Privacy utilities imported correctly.

The import of toPublicProfile and addPublicNameForAdmin is appropriate for implementing the privacy controls described in the PR objectives.


100-112: LGTM! Admin transformation applied consistently.

The use of addPublicNameForAdmin is appropriate for this admin-facing function, correctly preserving all profile fields (including realName and useDisplayName) while adding the computed publicName. The transformation is applied consistently to all profile objects with proper null handling for repliedToProfile on line 106. The existing hasAdminReply logic on lines 108-111 remains compatible with the transformed profiles.


13-37: Privacy transformation applied correctly; type safety enforces downstream compliance.

The transformation logic correctly applies toPublicProfile to all profile objects (parent comments, child comments, and replied-to profiles), explicitly removing PII fields (realName, useDisplayName) via TypeScript's Omit type and destructuring, while adding publicName. The null-safe handling of repliedToProfile on line 34 is correct.

The server function wrapper in src/fn/comments.ts properly exposes this data access function with authentication middleware and input validation. Since the return type is inferred via Awaited<ReturnType<typeof getComments>>, TypeScript automatically enforces that all downstream consumers receive and work with the transformed profile structure, preventing accidental access to stripped fields.

src/data-access/affiliates.ts (1)

227-228: LGTM! Fields added for admin context.

The addition of userRealName and useDisplayName to the projection correctly sources these fields from the profiles table and enables admin views to display both the alias and real name for moderation purposes.

src/utils/alias-generator.ts (1)

483-488: LGTM! Alias format meets requirements.

The format "Adjective Noun ##" with 237 adjectives × 231 nouns × 90 numbers provides ~4.9 million unique combinations, which is sufficient for reasonable scale. The implementation correctly generates human-readable aliases as specified in issue #33.

src/fn/profiles.ts (1)

20-21: LGTM! Schema extension aligns with privacy requirements.

The addition of realName (optional, nullable string) and useDisplayName (optional boolean) correctly extends the profile update schema to support the privacy toggle feature. The nullable nature of realName appropriately supports the "clear for privacy" use case.

src/routes/profile/edit.tsx (3)

420-445: Real Name input with clear functionality works correctly.

The implementation properly:

  • Displays the input with the current value
  • Shows the clear (X) button only when realName has a value
  • Clears the field when the button is clicked
  • Provides helpful context about affiliate program requirements

The helper text clearly communicates the privacy implications.


397-418: LGTM! Use Display Name toggle implemented correctly.

The Controller-based implementation properly binds the Switch component to the form state, with clear descriptive text explaining the privacy behavior. This follows React Hook Form best practices.


504-514: LGTM! Consistent Controller pattern for isPublicProfile toggle.

The update to use Controller for the isPublicProfile toggle maintains consistency with the new useDisplayName toggle implementation and follows React Hook Form best practices.

@amoscicki amoscicki closed this Dec 22, 2025
@amoscicki amoscicki reopened this Dec 22, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/routes/profile/$userId.tsx (1)

67-72: Consider using cache invalidation instead of staleTime: 0 for better performance.

Setting staleTime: 0 forces a refetch on every component mount, which can impact performance. While this fixes the useDisplayName toggle behavior, a more efficient approach would be to invalidate the cache only when the toggle actually changes (e.g., in the profile edit form after a successful update):

// In the profile edit form after toggle update:
await queryClient.invalidateQueries({ queryKey: ["profile", "public", userId] });

This way, the profile page can benefit from caching during normal navigation while still updating immediately when the user changes their display preference.

src/routes/learn/$slug/-components/comment-list.tsx (2)

44-47: Consider replacing any with proper typing.

The comment prop uses any type, which bypasses TypeScript's type safety. Since this PR introduces access to new properties like publicName, proper typing would help catch potential issues at compile time and improve IDE support.

🔎 Suggested type improvement
+import type { CommentsWithUser } from "~/data-access/comments";
+
+type CommentWithProfile = CommentsWithUser[number];
+
 interface CommentItemProps {
-  comment: any;
+  comment: CommentWithProfile;
   level?: number;
 }

Note: Verify that CommentsWithUser[number] provides the correct shape including profile.publicName and repliedToProfile.publicName.


359-361: Optional: Use consistent avatar seed format.

The reply form avatar uses user?.id || "user" as the seed for initials generation, while other avatars use names (e.g., publicName). Using a numeric ID may not generate meaningful initials.

Consider using the current user's public name for consistency:

                                  src={
-                                    comment.profile.image ??
-                                    `https://api.dicebear.com/9.x/initials/svg?seed=${user?.id || "user"}&backgroundColor=6366f1&textColor=ffffff`
+                                    user?.image ??
+                                    `https://api.dicebear.com/9.x/initials/svg?seed=${user?.publicName || user?.id || "user"}&backgroundColor=6366f1&textColor=ffffff`
                                  }

Note: Verify that user object includes image and publicName properties from the auth context.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0378435 and 8257412.

📒 Files selected for processing (2)
  • src/routes/learn/$slug/-components/comment-list.tsx
  • src/routes/profile/$userId.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: When invoking a TanStack server function, pass an object with a data property containing parameters
All card components should use the shadcn Card component with CardTitle, CardDescription, and other shadcn Card subcomponents
All server-side operations should use TanStack Start server functions created with createServerFn instead of direct backend logic
Form handling should use React Hook Form with Zod validation

**/*.{ts,tsx}: Never hard code magic numbers into code; consolidate them to the top of the file or in /src/config/index.ts to keep the code clean
Never allow a file to exceed 1,000 lines of code; split overly large files into smaller modular components to maintain code maintainability

Files:

  • src/routes/profile/$userId.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
**/routes/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

**/routes/**/*.tsx: Pages should use the Page component and PageHeader component when possible
Implement error boundaries at the route level using DefaultCatchBoundary

Files:

  • src/routes/profile/$userId.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
**/{components,routes}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Component styling should follow shadcn/ui patterns with Tailwind CSS v4

Files:

  • src/routes/profile/$userId.tsx
  • src/routes/learn/$slug/-components/comment-list.tsx
🧬 Code graph analysis (1)
src/routes/profile/$userId.tsx (3)
src/fn/profiles.ts (1)
  • getPublicProfileFn (43-50)
src/routes/admin/-components/page.tsx (1)
  • Page (7-21)
src/components/ui/avatar.tsx (1)
  • AvatarFallback (50-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Playwright Tests
🔇 Additional comments (4)
src/routes/profile/$userId.tsx (2)

138-146: Excellent defensive implementation for publicName and initials!

This change directly addresses the past review comment about undefined access. The implementation now includes all the recommended safety measures:

  • Triple fallback chain for publicName (publicName → displayName → "User")
  • .trim() removes leading/trailing whitespace
  • .split(/\s+/) handles multiple spaces correctly
  • .filter((n) => n.length > 0) removes empty segments
  • || "U" provides a final fallback if no initials can be extracted

This ensures robust handling of edge cases like empty strings, extra spaces, or malformed data.


153-159: LGTM! Consistent and privacy-aware publicName usage throughout the UI.

The changes correctly replace direct name references with publicName across all user-facing areas:

  • PageHeader: Title, highlighted word, and description now use publicName
  • Avatar: Alt text and fallback initials derived from publicName
  • Profile display: Main heading and descriptions reference publicName
  • Empty states: Messages appropriately reference publicName

This implementation aligns perfectly with the PR objectives to add privacy controls and prevent exposure of real names. The consistent usage ensures a uniform experience across all sections of the profile page.

Also applies to: 181-185, 194-194, 265-265, 359-359

src/routes/learn/$slug/-components/comment-list.tsx (2)

42-42: LGTM: Type-only import.

Using import type for type-only imports is a TypeScript best practice that clearly signals the import is only used for type checking.


196-196: The review comment is incorrect. The publicName field is guaranteed to be a string type for all profiles returned by getComments(). The data-access layer (lines 30, 33, 34 in src/data-access/comments.ts) applies toPublicProfile() to all profile objects, which returns { publicName: string } (computed by getPublicName() that returns either displayName || "Anonymous" or realName). Therefore, lines 211 and 239 cannot display empty or "undefined" values—the concern about missing fallbacks is invalid since publicName is always a non-null string by contract.

Likely an incorrect or invalid review comment.

@webdevcody webdevcody merged commit 4076c32 into webdevcody:main Dec 22, 2025
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace real names with auto-generated aliases in course discussions

2 participants