Skip to content

Wallet auth#93

Merged
respp merged 21 commits intoStellar-Rent:mainfrom
Benjtalkshow:wallet-auth
Jul 9, 2025
Merged

Wallet auth#93
respp merged 21 commits intoStellar-Rent:mainfrom
Benjtalkshow:wallet-auth

Conversation

@Benjtalkshow
Copy link
Contributor

@Benjtalkshow Benjtalkshow commented Jun 29, 2025

StellarRent Logo

Pull Request | StellarRent

Closes #82

📝 Summary

Implemented secure wallet-based authentication for Stellar wallets using challenge-response mechanism. Users can now authenticate with Freighter wallet as an alternative to email/password.

Files Added/Modified

Backend

src/
├── controllers/wallet-auth.controller.ts     # Auth endpoints
├── services/wallet-auth.service.ts           # Auth logic  
├── services/wallet-challenge.service.ts      # Challenge management
├── routes/wallet-auth.routes.ts              # Routes
├── types/wallet-auth.types.ts                # Types
├── validators/wallet-auth.validator.ts       # Validation
├── middleware/rateLimiter.ts         # Rate limiting

🔄 Changes Made

  1. POST /auth/challenge endpoint that generates cryptographically secure challenges
  • Challenges stored in Supabase with 5-minute expiration
  • Automatic cleanup of expired challenges
  1. POST /auth/wallet endpoint for wallet authentication
  • Uses Stellar SDK to verify signatures against challenges
  • Generates JWT tokens upon successful verification
  1. Rate limiting (10 auth attempts per 15 minutes, 20 challenges per 5 minutes)
  • Secure HTTP-only cookies for JWT storage
  • Challenge expiration and cleanup
  • Input validation with Zod schemas
  1. New tables: wallet_challenges, wallet_users, profiles
  • Maintains compatibility with existing user profiles
  • Supports both Web2 and wallet authentication simultaneously
  1. Specific error messages for different failure scenarios
  • Proper HTTP status codes
  • Comprehensive logging

Key Features

  • Secure Challenge-Response Auth: Cryptographic wallet authentication
  • Smart UX: Auto-authentication for returning users, clear button states
  • Official Freighter Integration: Uses @stellar/freighter-api (not window.freighterApi) for reliable wallet detection
  • Complete Security: JWT tokens, rate limiting, signature verification

Frontend

components/auth/
├── WalletAuthButton.tsx                      # NEW: Wallet auth component
└── RegisterForm.tsx                          # MODIFIED: Added wallet option

hooks/
├── useWallet.ts                              # NEW: Wallet connection hook
└── use-auth.tsx                              # MODIFIED: Added wallet auth

lib/
└── freighter-utils.ts                        # NEW: Freighter API utilities

services/
└── api.ts                                    # MODIFIED: Added wallet endpoints

Authentication Flow

  1. Request challenge → 2. Create transaction with challenge → 3. Sign with Freighter → 4. Verify signature → 5. Issue JWT

✅ Testing Checklist

  • Unit tests added/modified
  • Integration tests performed
  • Manual tests executed
  • All tests pass in CI/CD

Proof

Watch Video

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced wallet-based authentication using Stellar wallets (Freighter) alongside traditional email registration and login.
    • Added UI components and flows for connecting, authenticating, and managing wallet-based user sessions.
    • Implemented backend support for wallet authentication, including challenge generation, verification, and user profile management.
    • Added rate limiting for wallet authentication endpoints for enhanced security.
    • Added a protected dashboard layout with access control based on authentication status and type.
    • Added a network status indicator component displaying blockchain network information.
  • Enhancements

    • Updated user profile and authentication state management to support wallet public keys and authentication type.
    • Improved error handling and user feedback for wallet connection and authentication processes.
    • Enhanced API communication with detailed logging and error parsing for wallet authentication.
    • Refined wallet connection hooks with improved state management, error handling, and refresh capabilities.
    • Added utility functions for interacting with the Freighter wallet extension.
    • Updated UI text to English and improved wallet authentication user experience.
  • Bug Fixes

    • Improved error messages and validation for wallet authentication requests.
  • Chores

    • Added new dependencies for Stellar SDK, wallet authentication, and validation libraries.
    • Updated type definitions and interfaces to accommodate wallet authentication features.
    • Refactored wallet connection hooks and introduced utility functions for Freighter wallet integration.
    • Added database migrations for wallet authentication and user profiles.
    • Added cleanup scheduler for expired wallet challenges.
    • Added comprehensive tests for wallet authentication system.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 29, 2025

Walkthrough

This change introduces a full-stack implementation of wallet-based authentication using Stellar wallets (with Freighter support). It adds backend challenge generation, signature verification, JWT issuance, user synchronization with Supabase, and frontend integration including UI components, hooks, and utilities for wallet connection and authentication. Database migrations, validation, and rate limiting are also included.

Changes

File(s) / Group Change Summary
apps/backend/package.json, apps/web/package.json Added dependencies for Stellar SDK, UUID, Joi, node-cron, and Freighter API.
apps/backend/database/migrations/00004_create_wallet_auth_tables.sql Added migration for wallet_challenges and wallet_users tables, indexes, policies, and cleanup function.
apps/backend/database/migrations/00005_create_profile_table.sql Added migration for profiles table with RLS, indexes, and update timestamp trigger.
apps/backend/src/config/supabase.ts Extended Supabase types with profiles, wallet_challenges, and wallet_users tables.
apps/backend/src/controllers/wallet-auth.controller.ts Added controller for wallet challenge generation and authentication endpoints.
apps/backend/src/index.ts Registered new /api/auth route for wallet authentication; commented out /api/bookings.
apps/backend/src/middleware/rateLimiter.ts Added specialized rate limiters for wallet auth and challenge endpoints.
apps/backend/src/routes/wallet-auth.routes.ts Introduced wallet authentication routes with validation and rate limiting.
apps/backend/src/services/wallet-challenge.service.ts, wallet-auth.service.ts Implemented challenge generation, validation, wallet signature verification, JWT issuance, and user/profile synchronization.
apps/backend/src/types/wallet-auth.types.ts Defined TypeScript types and Zod schemas for wallet auth flows.
apps/backend/src/validators/wallet-auth.validator.ts Added validation middleware for challenge and wallet login requests.
apps/backend/src/types/wallet-error-types.ts Added custom error classes for wallet authentication errors.
apps/backend/src/services/cleanup-schedular.ts Added scheduled cleanup job for expired wallet challenges using node-cron.
apps/backend/tests/integration/wallet-auth.test.ts Added comprehensive integration tests for wallet authentication services, API endpoints, and error types.
apps/web/src/components/auth/RegisterForm.tsx Enhanced registration form with wallet authentication option and English translations.
apps/web/src/components/auth/WalletAuthButton.tsx New UI component for wallet connection and authentication with Freighter.
apps/web/src/hooks/auth/use-auth.tsx Extended auth context to support wallet login, token storage, and auth type tracking.
apps/web/src/hooks/useWallet.ts Refactored wallet hook for improved state, error handling, and Freighter API abstraction.
apps/web/src/lib/freighter-utils.ts New utility module for interacting with Freighter wallet extension.
apps/web/src/services/api.ts Added API methods for requesting challenges and authenticating wallets, with type and error handling improvements.
apps/web/src/types/index.ts Extended UserProfile with publicKey and global freighterApi type declaration.
apps/web/src/app/dashboard/layout.tsx Added DashboardLayout component wrapping children with ProtectedRoute.
apps/web/src/app/dashboard/page.tsx Added simple DashboardPage component.
apps/web/src/hooks/auth/procted-route.ts.ts Added ProtectedRoute component to guard routes based on authentication status and type.
apps/web/src/components/NetworkStatus.tsx Added component to display current blockchain network status based on wallet connection.
apps/web/src/lib/network-utils.ts Added utility functions for Stellar network passphrase and network name handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebApp
    participant Freighter
    participant Backend
    participant Supabase

    User->>WebApp: Clicks "Connect Wallet"
    WebApp->>Freighter: Requests public key
    Freighter-->>WebApp: Returns public key
    WebApp->>Backend: POST /api/auth/challenge { publicKey }
    Backend->>Supabase: Store challenge (wallet_challenges)
    Backend-->>WebApp: Returns challenge
    WebApp->>Freighter: Signs challenge (as Stellar tx memo)
    Freighter-->>WebApp: Returns signed tx XDR
    WebApp->>Backend: POST /api/auth/wallet { publicKey, signedTransaction, challenge }
    Backend->>Supabase: Validate challenge, verify signature
    Backend->>Supabase: Find or create wallet user/profile
    Backend-->>WebApp: Returns JWT token, user data
    WebApp->>User: Authenticated, redirects to dashboard
Loading

Assessment against linked issues

Objective Addressed Explanation
Backend: Challenge generation endpoint with 5-minute expiration tied to public key (#82)
Frontend: Wallet connection, challenge signing, and sending signature for verification (#82)
Backend: Signature verification, JWT issuance, and secure cookie storage (#82)
Frontend: useAuth hook updated for wallet login state and session management (#82)
Error handling for wallet not installed, user rejection, invalid/expired challenges, and signature (#82)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Possibly related PRs

Poem

In the moonlit fields, a rabbit hops with glee,
“Wallets now connect—secure as can be!
Stellar stars above, Freighter by my side,
JWTs in my pocket, on the blockchain I ride.
From frontend to backend, the journey’s complete—
Authentication’s a treat, with security so neat!”
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@Benjtalkshow Benjtalkshow marked this pull request as ready for review June 30, 2025 22:19
Copy link
Contributor

@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: 12

🔭 Outside diff range comments (1)
apps/backend/package.json (1)

15-15: Remove duplicate Stellar SDK dependency.

You have both @stellar/stellar-sdk (line 15) and stellar-sdk (line 26) in your dependencies. The @stellar/stellar-sdk is the official package name, while stellar-sdk is likely the deprecated version. Having both can cause conflicts and increase bundle size.

Remove the duplicate dependency:

-    "stellar-sdk": "^13.3.0",

Also applies to: 26-26

🧹 Nitpick comments (2)
apps/backend/src/index.ts (1)

35-35: Remove commented code.

Clean up commented code if it's no longer needed, or add a TODO comment explaining why it's temporarily disabled.

-// app.use('/api/bookings', bookingRoutes);
apps/backend/src/controllers/wallet-auth.controller.ts (1)

29-34: Consider adding domain and path attributes to the cookie.

For better security and control, consider explicitly setting the domain and path attributes.

     res.cookie('auth-token', authResponse.token, {
       httpOnly: true,
       secure: process.env.NODE_ENV === 'production',
       sameSite: 'strict',
       maxAge: 7 * 24 * 60 * 60 * 1000,
+      path: '/',
+      domain: process.env.COOKIE_DOMAIN || undefined,
     });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e9aae0 and 6744552.

📒 Files selected for processing (10)
  • apps/backend/package.json (1 hunks)
  • apps/backend/src/config/supabase.ts (2 hunks)
  • apps/backend/src/controllers/wallet-auth.controller.ts (1 hunks)
  • apps/backend/src/index.ts (2 hunks)
  • apps/backend/src/middleware/rateLimiter.ts (1 hunks)
  • apps/backend/src/routes/wallet-auth.routes.ts (1 hunks)
  • apps/backend/src/services/wallet-auth.services.ts (1 hunks)
  • apps/backend/src/services/wallet-challenge.service.ts (1 hunks)
  • apps/backend/src/types/wallet-auth.types.ts (1 hunks)
  • apps/backend/src/validators/wallet-auth.validator.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
apps/backend/src/validators/wallet-auth.validator.ts (1)
apps/backend/src/types/wallet-auth.types.ts (2)
  • challengeRequestSchema (6-8)
  • walletLoginSchema (13-17)
apps/backend/src/services/wallet-auth.services.ts (3)
apps/backend/src/config/supabase.ts (1)
  • supabase (205-217)
apps/backend/src/types/wallet-auth.types.ts (2)
  • WalletLoginRequest (23-23)
  • WalletAuthResponse (55-62)
apps/backend/src/services/wallet-challenge.service.ts (2)
  • getValidChallenge (56-73)
  • removeChallenge (78-84)
apps/backend/src/controllers/wallet-auth.controller.ts (1)
apps/backend/src/types/wallet-auth.types.ts (2)
  • ChallengeRequest (22-22)
  • WalletLoginRequest (23-23)
apps/backend/src/routes/wallet-auth.routes.ts (2)
apps/backend/src/middleware/rateLimiter.ts (2)
  • challengeRateLimit (30-38)
  • walletAuthRateLimit (17-25)
apps/backend/src/validators/wallet-auth.validator.ts (2)
  • validateChallengeRequest (5-18)
  • validateWalletLogin (20-33)
🔇 Additional comments (13)
apps/backend/src/routes/wallet-auth.routes.ts (1)

1-18: Well-structured wallet authentication routes.

The route definitions follow good practices with proper middleware chaining (rate limiting → validation → controller). The separation of challenge generation and wallet authentication endpoints is logical and secure.

apps/backend/src/middleware/rateLimiter.ts (1)

18-19: Appropriate rate limiting values for wallet authentication.

The rate limits seem well-calibrated for security:

  • 10 wallet auth attempts per 15 minutes prevents brute force attacks
  • 20 challenge requests per 5 minutes allows legitimate usage while preventing spam

These values strike a good balance between security and usability.

Also applies to: 31-32

apps/backend/src/validators/wallet-auth.validator.ts (2)

5-18: Well-implemented validation middleware.

The validation logic is correctly structured with proper error handling. The middleware appropriately validates the request body against Zod schemas and provides clear error messages with HTTP 400 status for validation failures.


20-33: Consistent validation pattern maintained.

The validateWalletLogin middleware follows the same pattern as validateChallengeRequest, maintaining consistency in error handling and response structure throughout the validation layer.

apps/backend/src/services/wallet-auth.services.ts (2)

10-27: LGTM! Robust signature verification implementation.

The function properly handles errors and uses the Stellar SDK correctly for signature verification.


86-102: LGTM! Secure JWT generation with proper configuration checks.

Good practice checking for the JWT_SECRET environment variable and using a reasonable expiration time.

apps/backend/src/controllers/wallet-auth.controller.ts (1)

6-18: LGTM! Clean challenge generation endpoint.

Good error handling and appropriate status codes.

apps/backend/src/services/wallet-challenge.service.ts (4)

10-25: LGTM! Appropriate error handling for cleanup operation.

Silent failure is acceptable for cleanup operations to avoid disrupting the main flow.


30-51: LGTM! Well-structured challenge generation.

Good practice to clean up expired challenges before creating new ones. Proper error handling.


56-73: LGTM! Clear and secure challenge validation.

Good use of timestamp comparison to ensure challenge validity.


89-91: LGTM! Useful for scheduled cleanup tasks.

apps/backend/src/config/supabase.ts (1)

27-192: LGTM! Well-structured database schema for wallet authentication.

The new tables provide a solid foundation for wallet-based authentication with appropriate fields for challenges, users, and profiles.

apps/backend/src/types/wallet-auth.types.ts (1)

33-46: Consider making some UserProfile fields required.

The verification_status and last_active fields are not optional in the interface but are stored as required in the database schema. This inconsistency could lead to runtime errors.

"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"jest-mock-extended": "^4.0.0",
"joi": "^17.13.3",
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Question the need for Joi alongside Zod.

You're adding joi validation library while already using zod (line 28). This introduces inconsistency in validation approaches across the codebase.

Consider removing Joi and using Zod consistently throughout the application:

-    "joi": "^17.13.3",

If Joi is truly needed for specific use cases, please document why both validation libraries are necessary.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"joi": "^17.13.3",
🤖 Prompt for AI Agents
In apps/backend/package.json at line 23, you have added the Joi validation
library while Zod is already used at line 28, causing inconsistency. Review the
codebase to determine if Joi is essential; if not, remove the Joi dependency to
maintain a single validation library (Zod). If Joi must remain for specific
reasons, add clear documentation explaining why both Joi and Zod are required.

Comment on lines 33 to +34
app.use('/auth', authRoutes);
app.use('/api/bookings', bookingRoutes);
app.use('/api/auth', walletAuthRoutes);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Clarify the dual authentication route structure.

Having both /auth and /api/auth routes could confuse API consumers. Consider consolidating authentication routes under a single path prefix for consistency.

Consider one of these approaches:

Option 1: Consolidate under /api/auth

-app.use('/auth', authRoutes);
-app.use('/api/auth', walletAuthRoutes);
+app.use('/api/auth', authRoutes);
+app.use('/api/auth', walletAuthRoutes);

Option 2: Use distinct paths

 app.use('/auth', authRoutes);
-app.use('/api/auth', walletAuthRoutes);
+app.use('/auth/wallet', walletAuthRoutes);

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

🤖 Prompt for AI Agents
In apps/backend/src/index.ts around lines 33 to 34, the use of both '/auth' and
'/api/auth' routes for authentication can confuse API consumers. To fix this,
consolidate authentication routes under a single consistent path prefix by
either merging all auth routes under '/api/auth' or clearly separating them with
distinct and descriptive paths. Update the route registrations accordingly to
maintain clarity and consistency.

Comment on lines +30 to +38
export const challengeRateLimit = rateLimit({
windowMs: 5 * 60 * 1000,
max: 20,
message: {
error: 'Too many challenge requests, please try again later.',
},
standardHeaders: true,
legacyHeaders: false,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add keyGenerator for consistency with development environments.

Same issue as above - the challenge rate limiter needs the keyGenerator function for development environment compatibility.

 export const challengeRateLimit = rateLimit({
   windowMs: 5 * 60 * 1000,
   max: 20,
   message: {
     error: 'Too many challenge requests, please try again later.',
   },
   standardHeaders: true,
   legacyHeaders: false,
+  keyGenerator: (req): string => {
+    // Handle localhost and development environments
+    return req.ip || 'localhost';
+  },
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const challengeRateLimit = rateLimit({
windowMs: 5 * 60 * 1000,
max: 20,
message: {
error: 'Too many challenge requests, please try again later.',
},
standardHeaders: true,
legacyHeaders: false,
});
export const challengeRateLimit = rateLimit({
windowMs: 5 * 60 * 1000,
max: 20,
message: {
error: 'Too many challenge requests, please try again later.',
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req): string => {
// Handle localhost and development environments
return req.ip || 'localhost';
},
});
🤖 Prompt for AI Agents
In apps/backend/src/middleware/rateLimiter.ts between lines 30 and 38, the
challengeRateLimit configuration is missing a keyGenerator function, which is
needed for consistent behavior across development environments. Add a
keyGenerator function to the rateLimit options that returns a unique key (such
as the user's IP address or another identifier) to ensure the rate limiter works
correctly in all environments.

Comment on lines +17 to +25
export const walletAuthRateLimit = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
message: {
error: 'Too many wallet authentication attempts, please try again later.',
},
standardHeaders: true,
legacyHeaders: false,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add keyGenerator for consistency with development environments.

The new rate limiters are missing the keyGenerator function present in the original rateLimiter (lines 8-11). This could cause issues in development environments where IP detection might not work properly.

Add the keyGenerator to both new rate limiters:

 export const walletAuthRateLimit = rateLimit({
   windowMs: 15 * 60 * 1000, // 15 minutes
   max: 10,
   message: {
     error: 'Too many wallet authentication attempts, please try again later.',
   },
   standardHeaders: true,
   legacyHeaders: false,
+  keyGenerator: (req): string => {
+    // Handle localhost and development environments
+    return req.ip || 'localhost';
+  },
 });
🤖 Prompt for AI Agents
In apps/backend/src/middleware/rateLimiter.ts around lines 17 to 25, the
walletAuthRateLimit configuration is missing the keyGenerator function that is
present in the original rateLimiter between lines 8 and 11. To fix this, add the
same keyGenerator function to walletAuthRateLimit to ensure consistent client
identification, especially in development environments where IP detection may
fail.

Comment on lines 121 to 125
await removeChallenge(storedChallenge.id);
const walletUser = await getOrCreateWalletUser(publicKey);
const token = generateJWT(walletUser.id, publicKey);
const profile = await getUserProfile(walletUser.id);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Move challenge removal to after all operations succeed.

The challenge is removed immediately after signature verification, but if subsequent operations (user creation, JWT generation) fail, the user would need to request a new challenge even though their signature was valid.

   const isValidSignature = await verifySignature(publicKey, challenge, signature);
   if (!isValidSignature) {
     throw new Error('Invalid signature');
   }
-  await removeChallenge(storedChallenge.id);
   const walletUser = await getOrCreateWalletUser(publicKey);
   const token = generateJWT(walletUser.id, publicKey);
   const profile = await getUserProfile(walletUser.id);
+  
+  // Remove challenge only after all operations succeed
+  await removeChallenge(storedChallenge.id);

   return {
     token,
🤖 Prompt for AI Agents
In apps/backend/src/services/wallet-auth.services.ts around lines 121 to 125,
the removal of the challenge occurs immediately after signature verification,
which can cause issues if later operations fail. Move the call to
removeChallenge so that it happens only after all subsequent operations
(getOrCreateWalletUser, generateJWT, getUserProfile) complete successfully,
ensuring the challenge is only removed when the entire process succeeds.

Comment on lines 40 to 43
await supabase
.from('wallet_users')
.update({ updated_at: new Date().toISOString() })
.eq('id', existingUser.id);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for the update operation.

The update operation could fail, but the error is not being checked. This could lead to silent failures.

-    await supabase
+    const { error: updateError } = await supabase
       .from('wallet_users')
       .update({ updated_at: new Date().toISOString() })
       .eq('id', existingUser.id);
+
+    if (updateError) {
+      console.error('Error updating wallet user:', updateError);
+      throw new Error('Failed to update wallet user');
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await supabase
.from('wallet_users')
.update({ updated_at: new Date().toISOString() })
.eq('id', existingUser.id);
const { error: updateError } = await supabase
.from('wallet_users')
.update({ updated_at: new Date().toISOString() })
.eq('id', existingUser.id);
if (updateError) {
console.error('Error updating wallet user:', updateError);
throw new Error('Failed to update wallet user');
}
🤖 Prompt for AI Agents
In apps/backend/src/services/wallet-auth.services.ts around lines 40 to 43, the
update operation on the 'wallet_users' table does not handle potential errors.
Modify the code to capture the response from the update call, check if there is
an error, and handle it appropriately, such as by logging the error or throwing
an exception to prevent silent failures.

Comment on lines 32 to 68
async function getOrCreateWalletUser(publicKey: string) {
const { data: existingUser, error: fetchError } = await supabase
.from('wallet_users')
.select('*')
.eq('public_key', publicKey)
.single();

if (existingUser && !fetchError) {
await supabase
.from('wallet_users')
.update({ updated_at: new Date().toISOString() })
.eq('id', existingUser.id);

return existingUser;
}

const { data: newUser, error: createError } = await supabase
.from('wallet_users')
.insert({
public_key: publicKey,
})
.select()
.single();

if (createError || !newUser) {
console.error('Error creating wallet user:', createError);
throw new Error('Failed to create wallet user');
}

await supabase.from('profiles').insert({
user_id: newUser.id,
verification_status: 'unverified',
last_active: new Date().toISOString(),
});

return newUser;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using upsert to handle potential race conditions.

The current implementation has a race condition where two concurrent requests with the same public key could both try to create a new user. Consider using Supabase's upsert functionality.

async function getOrCreateWalletUser(publicKey: string) {
-  const { data: existingUser, error: fetchError } = await supabase
-    .from('wallet_users')
-    .select('*')
-    .eq('public_key', publicKey)
-    .single();
-
-  if (existingUser && !fetchError) {
-    await supabase
-      .from('wallet_users')
-      .update({ updated_at: new Date().toISOString() })
-      .eq('id', existingUser.id);
-
-    return existingUser;
-  }
-
-  const { data: newUser, error: createError } = await supabase
+  const { data: user, error } = await supabase
     .from('wallet_users')
-    .insert({
+    .upsert({
       public_key: publicKey,
+      updated_at: new Date().toISOString(),
     })
     .select()
     .single();

-  if (createError || !newUser) {
-    console.error('Error creating wallet user:', createError);
-    throw new Error('Failed to create wallet user');
+  if (error || !user) {
+    console.error('Error upserting wallet user:', error);
+    throw new Error('Failed to upsert wallet user');
   }

-  await supabase.from('profiles').insert({
-    user_id: newUser.id,
-    verification_status: 'unverified',
-    last_active: new Date().toISOString(),
-  });
+  // Check if profile exists
+  const { data: profile } = await supabase
+    .from('profiles')
+    .select('*')
+    .eq('user_id', user.id)
+    .single();
+
+  if (!profile) {
+    const { error: profileError } = await supabase.from('profiles').insert({
+      user_id: user.id,
+      verification_status: 'unverified',
+      last_active: new Date().toISOString(),
+    });
+
+    if (profileError) {
+      console.error('Error creating user profile:', profileError);
+      throw new Error('Failed to create user profile');
+    }
+  }

-  return newUser;
+  return user;
}

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

🤖 Prompt for AI Agents
In apps/backend/src/services/wallet-auth.services.ts between lines 32 and 68,
the current getOrCreateWalletUser function has a race condition because it
separately checks for an existing user and then inserts a new one, which can
cause duplicate entries if two requests run concurrently. To fix this, replace
the separate select and insert calls with a single upsert operation on the
wallet_users table using the publicKey as the conflict target. This will
atomically insert or update the user record, preventing duplicates and race
conditions.

Comment on lines +36 to +39
res.status(200).json({
user: authResponse.user,
token: authResponse.token,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Security: Don't expose JWT token in response body when using HTTP-only cookies.

Returning the token in the response body defeats the security benefits of HTTP-only cookies, as it makes the token accessible to JavaScript and vulnerable to XSS attacks.

     res.status(200).json({
       user: authResponse.user,
-      token: authResponse.token,
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.status(200).json({
user: authResponse.user,
token: authResponse.token,
});
res.status(200).json({
user: authResponse.user,
});
🤖 Prompt for AI Agents
In apps/backend/src/controllers/wallet-auth.controller.ts around lines 36 to 39,
the JWT token is being sent in the JSON response body, which exposes it to
JavaScript and increases XSS risk. To fix this, remove the token from the JSON
response and instead set it as an HTTP-only cookie on the response object. Keep
only the user information in the JSON response body.

Comment on lines +78 to +84
export async function removeChallenge(challengeId: string): Promise<void> {
const { error } = await supabase.from('wallet_challenges').delete().eq('id', challengeId);

if (error) {
console.error('Error removing challenge:', error);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider throwing an error when challenge removal fails.

Failed challenge removal could allow challenge reuse, which is a security concern.

 export async function removeChallenge(challengeId: string): Promise<void> {
   const { error } = await supabase.from('wallet_challenges').delete().eq('id', challengeId);

   if (error) {
     console.error('Error removing challenge:', error);
+    throw new Error('Failed to remove challenge');
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function removeChallenge(challengeId: string): Promise<void> {
const { error } = await supabase.from('wallet_challenges').delete().eq('id', challengeId);
if (error) {
console.error('Error removing challenge:', error);
}
}
export async function removeChallenge(challengeId: string): Promise<void> {
const { error } = await supabase.from('wallet_challenges').delete().eq('id', challengeId);
if (error) {
console.error('Error removing challenge:', error);
throw new Error('Failed to remove challenge');
}
}
🤖 Prompt for AI Agents
In apps/backend/src/services/wallet-challenge.service.ts around lines 78 to 84,
the current code logs an error when challenge removal fails but does not stop
execution. To fix this, modify the function to throw an error if the deletion
operation returns an error, ensuring that failure to remove a challenge is
properly handled and prevents potential security issues.

Comment on lines 6 to 17
export const challengeRequestSchema = z.object({
publicKey: z.string().min(1, 'Public key is required'),
});

//===================
// Wallet login schema
//===================
export const walletLoginSchema = z.object({
publicKey: z.string().min(1, 'Public key is required'),
signature: z.string().min(1, 'Signature is required'),
challenge: z.string().min(1, 'Challenge is required'),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add Stellar public key format validation.

Consider adding specific validation for Stellar public keys to catch invalid formats early.

+const stellarPublicKeyRegex = /^G[A-Z2-7]{55}$/;
+
 export const challengeRequestSchema = z.object({
-  publicKey: z.string().min(1, 'Public key is required'),
+  publicKey: z.string().min(1, 'Public key is required').regex(stellarPublicKeyRegex, 'Invalid Stellar public key format'),
 });

 //===================
 // Wallet login schema
 //===================
 export const walletLoginSchema = z.object({
-  publicKey: z.string().min(1, 'Public key is required'),
+  publicKey: z.string().min(1, 'Public key is required').regex(stellarPublicKeyRegex, 'Invalid Stellar public key format'),
   signature: z.string().min(1, 'Signature is required'),
   challenge: z.string().min(1, 'Challenge is required'),
 });

🌐 Web query:

What is the correct regex pattern for validating Stellar public keys?

💡 Result:

The correct regex pattern for validating Stellar public keys corresponds to the key format described by the Stellar network specifications:

  • Stellar public keys are encoded in a specific StrKey format.
  • They are 56-character strings.
  • They start with the uppercase letter "G".
  • They consist of Base32 encoding (RFC4648) without padding, which means only characters A-Z (except I, L, O) and digits 2-7 are used.

A common, practical regex pattern to validate a Stellar public key is:

^G[A-Z2-7]{55}$

Explanation:

  • ^ asserts the start of the string.
  • G matches the mandatory first character for public keys.
  • [A-Z2-7]{55} matches the remaining 55 characters, all uppercase letters A-Z excluding invalid Base32 chars, and digits 2-7.
  • $ asserts the end of the string.

This pattern ensures the string is exactly 56 characters long and starts with "G", conforming to the Stellar public key format described by the official docs and supported libraries[2][8].

For more robust validation, including checksum verification and decoding, Stellar SDKs provide methods such as StrKey.isValidEd25519PublicKey(publicKey) in the JavaScript Stellar SDK, which internally handle base32 decoding and CRC16 checksum checks according to the StrKey specification[1][3][5].

In summary:

Aspect Detail
Length 56 characters
First Character G (uppercase)
Character Set Base32 (A-Z and digits 2-7)
Regex Example ^G[A-Z2-7]{55}$
Recommended Validation Use official SDK StrKey validation functions

This regex is sufficient to filter valid-looking Stellar public keys but should be used alongside checksum verification for full correctness as implemented in SDKs.

Citations:


Enforce Stellar public key format in validation schemas

To ensure only valid Stellar public keys pass validation, update apps/backend/src/types/wallet-auth.types.ts as follows:

• Define the Stellar regex at the top of the file:

const stellarPublicKeyRegex = /^G[A-Z2-7]{55}$/;

• In challengeRequestSchema, change:

- publicKey: z.string().min(1, 'Public key is required'),
+ publicKey: z.string()
+   .min(1, 'Public key is required')
+   .regex(stellarPublicKeyRegex, 'Invalid Stellar public key format'),

• In walletLoginSchema, change:

- publicKey: z.string().min(1, 'Public key is required'),
+ publicKey: z.string()
+   .min(1, 'Public key is required')
+   .regex(stellarPublicKeyRegex, 'Invalid Stellar public key format'),

For full checksum validation, consider using the Stellar SDK’s StrKey.isValidEd25519PublicKey() alongside this regex.

🤖 Prompt for AI Agents
In apps/backend/src/types/wallet-auth.types.ts between lines 6 and 17, the
current validation schemas only check for non-empty strings for public keys but
do not enforce the Stellar public key format. Define the Stellar public key
regex /^G[A-Z2-7]{55}$/ at the top of the file, then update the publicKey field
in both challengeRequestSchema and walletLoginSchema to use
z.string().regex(stellarPublicKeyRegex, 'Invalid Stellar public key format') to
enforce this pattern. Optionally, for stronger validation, integrate Stellar
SDK's StrKey.isValidEd25519PublicKey() in a custom validator.

Copy link
Contributor

@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: 9

♻️ Duplicate comments (2)
apps/backend/src/controllers/wallet-auth.controller.ts (1)

49-52: Security: Don't expose JWT token in response body when using HTTP-only cookies.

Returning the token in the response body defeats the security benefits of HTTP-only cookies, as it makes the token accessible to JavaScript and vulnerable to XSS attacks.

apps/backend/src/types/wallet-auth.types.ts (1)

4-13: Add Stellar public key format validation.

Consider adding specific validation for Stellar public keys to catch invalid formats early.

🧹 Nitpick comments (6)
apps/web/src/components/auth/RegisterForm.tsx (1)

47-50: Complete the translation to English for password strength indicator.

The password strength labels are still in Spanish while the rest of the UI has been translated to English.

-        {strength <= 2 && 'Débil'}
-        {strength > 2 && strength <= 4 && 'Media'}
-        {strength > 4 && 'Fuerte'}
+        {strength <= 2 && 'Weak'}
+        {strength > 2 && strength <= 4 && 'Medium'}
+        {strength > 4 && 'Strong'}
apps/backend/src/services/wallet-auth.service.ts (1)

46-48: Remove empty code blocks or add appropriate logging.

Empty code blocks reduce code clarity. Either add appropriate logging or remove these blocks.

For signature validation (lines 46-48):

-    if (!isValid) {
-    } else {
-    }

For profile error handling (line 95):

     if (profileError) {
+      console.error('Failed to create user profile:', profileError);
     }

Also applies to: 95-95

apps/web/src/components/auth/WalletAuthButton.tsx (1)

71-75: Remove redundant conditional branch.

Both branches of the conditional set the same state, making the check unnecessary.

     } else if (isConnected && publicKey && !isLoading) {
-      if (hasUsedWalletAuth(publicKey)) {
-        setAuthState('connected');
-      } else {
-        setAuthState('connected');
-      }
+      setAuthState('connected');
     }
apps/web/src/services/api.ts (1)

131-154: Consider the implications of empty email for wallet users.

The function sets email: '' for wallet users. This might cause issues if other parts of the application expect a valid email address. Consider either:

  1. Using a placeholder email format like ${publicKey.slice(0, 8)}@wallet.stellar
  2. Making email truly optional throughout the application
  3. Adding validation to handle empty emails in components that display user data
apps/web/src/hooks/useWallet.ts (1)

158-185: Consider optimizing the permission check in refreshConnection.

The isAllowed state is set based on address existence, but you're not actually checking permissions with checkFreighterPermission like in initializeWallet.

For consistency with initialization:

   const refreshConnection = async () => {
     setWalletState((prev) => ({ ...prev, isLoading: true }));
     try {
-      const [connectionStatus, addressResult, networkResult] = await Promise.all([
+      const [connectionStatus, permissionStatus, addressResult, networkResult] = await Promise.all([
         checkFreighterConnection(),
+        checkFreighterPermission(),
         getFreighterAddress(),
         getFreighterNetwork(),
       ]);

       setWalletState({
         isConnected: connectionStatus.isConnected && !!addressResult.address,
         publicKey: addressResult.address || null,
         network: networkResult.network || null,
         networkPassphrase: networkResult.networkPassphrase || null,
         isInstalled: connectionStatus.isInstalled,
-        isAllowed: !!addressResult.address,
+        isAllowed: permissionStatus.isAllowed,
         isLoading: false,
         error: null,
       });
apps/web/src/lib/freighter-utils.ts (1)

233-246: Consider simplifying the signed message handling.

The current logic is complex and the Buffer check might not work reliably in all environments.

Simplify the message handling:

     let signedMessage: string | undefined;

-    if (typeof signResult.signedMessage === 'string') {
-      signedMessage = signResult.signedMessage;
-    } else if (
-      signResult.signedMessage &&
-      typeof Buffer !== 'undefined' &&
-      typeof signResult.signedMessage.toString === 'function'
-    ) {
-      signedMessage = signResult.signedMessage.toString('utf8');
-    } else {
-      signedMessage = undefined;
-    }
+    // Handle both string and Buffer-like responses
+    if (signResult.signedMessage) {
+      if (typeof signResult.signedMessage === 'string') {
+        signedMessage = signResult.signedMessage;
+      } else if (signResult.signedMessage instanceof Uint8Array) {
+        signedMessage = new TextDecoder().decode(signResult.signedMessage);
+      } else if (typeof signResult.signedMessage.toString === 'function') {
+        signedMessage = signResult.signedMessage.toString();
+      }
+    }

This approach uses standard web APIs (TextDecoder) instead of Node.js-specific Buffer and handles Uint8Array which is more common in browser environments.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6744552 and e161cbc.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • apps/backend/database/migrations/00004_create_wallet_auth_tables.sql (1 hunks)
  • apps/backend/src/controllers/wallet-auth.controller.ts (1 hunks)
  • apps/backend/src/routes/wallet-auth.routes.ts (1 hunks)
  • apps/backend/src/services/wallet-auth.service.ts (1 hunks)
  • apps/backend/src/services/wallet-challenge.service.ts (1 hunks)
  • apps/backend/src/types/wallet-auth.types.ts (1 hunks)
  • apps/backend/src/validators/wallet-auth.validator.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/components/auth/RegisterForm.tsx (8 hunks)
  • apps/web/src/components/auth/WalletAuthButton.tsx (1 hunks)
  • apps/web/src/hooks/auth/use-auth.tsx (3 hunks)
  • apps/web/src/hooks/useWallet.ts (1 hunks)
  • apps/web/src/lib/freighter-utils.ts (1 hunks)
  • apps/web/src/services/api.ts (9 hunks)
  • apps/web/src/types/index.ts (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • apps/web/package.json
  • apps/web/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/backend/src/routes/wallet-auth.routes.ts
  • apps/backend/src/validators/wallet-auth.validator.ts
  • apps/backend/src/services/wallet-challenge.service.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/web/src/components/auth/RegisterForm.tsx (1)
apps/web/src/components/auth/WalletAuthButton.tsx (1)
  • WalletAuthButton (25-258)
apps/web/src/services/api.ts (2)
apps/backend/src/types/wallet-auth.types.ts (2)
  • ChallengeResponse (45-48)
  • WalletAuthResponse (27-43)
apps/web/src/types/index.ts (1)
  • UserProfile (39-55)
apps/web/src/hooks/useWallet.ts (1)
apps/web/src/lib/freighter-utils.ts (5)
  • checkFreighterConnection (41-68)
  • checkFreighterPermission (73-96)
  • getFreighterAddress (127-148)
  • getFreighterNetwork (153-176)
  • connectFreighter (101-122)
🔇 Additional comments (9)
apps/web/src/components/auth/RegisterForm.tsx (1)

75-78: Consider the navigation flow after wallet authentication in registration context.

The component navigates to /dashboard after successful wallet authentication, but this is a registration form. Consider whether new users should go through an onboarding flow or profile completion step first.

apps/web/src/components/auth/WalletAuthButton.tsx (1)

41-61: Well-implemented auto-authentication flow.

The auto-authentication logic is well-structured with proper error handling, state management, and user feedback. Good use of localStorage to track previous wallet authentication.

apps/web/src/services/api.ts (2)

49-71: LGTM! Well-structured interfaces for wallet authentication.

The interfaces correctly mirror the backend response types and provide clear structure for wallet authentication responses.


435-472: Well-implemented wallet authentication methods.

The new authentication methods follow the established patterns and properly handle errors. The integration with the wallet authentication flow is clean and consistent.

apps/web/src/hooks/useWallet.ts (2)

36-108: Excellent implementation of wallet initialization with proper cleanup.

The use of a mounted flag to prevent state updates after unmount and comprehensive error handling makes this implementation robust. The state management is thorough and well-structured.


110-147: Well-implemented connection logic with proper validation.

The connect function properly validates the returned address and maintains loading states throughout the operation. Error handling is comprehensive.

apps/web/src/hooks/auth/use-auth.tsx (2)

42-64: Good implementation of auth state persistence with error handling.

The error handling for localStorage parsing prevents the app from crashing on corrupted data, and the cleanup ensures a fresh start on error.


125-183: Well-implemented transaction building and signing flow.

The transaction construction is correct for authentication purposes, with proper error handling throughout the signing process. Using sequence number '0' is appropriate for this non-submittable auth transaction.

apps/web/src/lib/freighter-utils.ts (1)

41-176: Excellent error handling and API abstraction.

The consistent error handling pattern across all functions provides a clean abstraction over the Freighter API. The separation of connection checking, permission verification, and wallet operations is well-designed.

Comment on lines +28 to +36
if (!walletLoginData.publicKey) {
return res.status(400).json({ error: 'Missing publicKey' });
}
if (!walletLoginData.signedTransaction) {
return res.status(400).json({ error: 'Missing signedTransaction' });
}
if (!walletLoginData.challenge) {
return res.status(400).json({ error: 'Missing challenge' });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Remove redundant manual validation checks.

These manual validation checks should be handled by the validation middleware. If the middleware is properly configured, these checks are redundant.


🏁 Script executed:

#!/bin/bash
# Description: Verify validation middleware is applied to the wallet auth routes

# Search for wallet auth routes configuration
rg -A 10 "authenticateWallet|/wallet" apps/backend/src/routes/

# Check if validation middleware is imported and used
ast-grep --pattern 'walletLoginSchema'

Length of output: 2068


Remove redundant manual validation in controller

The validateWalletLogin middleware already enforces presence and type checks for publicKey, signedTransaction, and challenge. You can safely delete the manual if (!…){ return res.status(400)… } blocks in the controller.

  • File to update: apps/backend/src/controllers/wallet-auth.controller.ts
  • Remove lines 28–36 (the three manual if checks for publicKey, signedTransaction, and challenge)

Suggested diff:

--- a/apps/backend/src/controllers/wallet-auth.controller.ts
+++ b/apps/backend/src/controllers/wallet-auth.controller.ts
@@ -28,12 +28,6 @@ export const authenticateWalletController = async (req, res) => {
   const walletLoginData = req.body;
-
-  if (!walletLoginData.publicKey) {
-    return res.status(400).json({ error: 'Missing publicKey' });
-  }
-  if (!walletLoginData.signedTransaction) {
-    return res.status(400).json({ error: 'Missing signedTransaction' });
-  }
-  if (!walletLoginData.challenge) {
-    return res.status(400).json({ error: 'Missing challenge' });
-  }
 
   // …rest of authentication logic…
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!walletLoginData.publicKey) {
return res.status(400).json({ error: 'Missing publicKey' });
}
if (!walletLoginData.signedTransaction) {
return res.status(400).json({ error: 'Missing signedTransaction' });
}
if (!walletLoginData.challenge) {
return res.status(400).json({ error: 'Missing challenge' });
}
export const authenticateWalletController = async (req, res) => {
const walletLoginData = req.body;
// …rest of authentication logic…
}
🤖 Prompt for AI Agents
In apps/backend/src/controllers/wallet-auth.controller.ts between lines 28 and
36, remove the manual validation blocks that check for the presence of
publicKey, signedTransaction, and challenge, as these are already validated by
the validateWalletLogin middleware. Simply delete these if statements and their
corresponding return responses to avoid redundant validation.

Comment on lines +175 to +179
console.log(`🌐 API Call: ${options.method || 'GET'} ${API_BASE_URL}${endpoint}`);
if (options.body) {
console.log('📤 Request body:', options.body);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove request body logging to prevent sensitive data exposure.

Logging request bodies can expose sensitive information like passwords, authentication tokens, or private keys in the console. This is a security risk, especially in production.

Remove the request body logging or make it conditional:

-  console.log(`🌐 API Call: ${options.method || 'GET'} ${API_BASE_URL}${endpoint}`);
-  if (options.body) {
-    console.log('📤 Request body:', options.body);
-  }
+  if (process.env.NODE_ENV === 'development') {
+    console.log(`🌐 API Call: ${options.method || 'GET'} ${API_BASE_URL}${endpoint}`);
+    // Never log sensitive endpoints
+    const sensitiveEndpoints = ['/auth/', '/wallet/'];
+    const isSensitive = sensitiveEndpoints.some(e => endpoint.includes(e));
+    if (options.body && !isSensitive) {
+      console.log('📤 Request body:', options.body);
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(`🌐 API Call: ${options.method || 'GET'} ${API_BASE_URL}${endpoint}`);
if (options.body) {
console.log('📤 Request body:', options.body);
}
if (process.env.NODE_ENV === 'development') {
console.log(`🌐 API Call: ${options.method || 'GET'} ${API_BASE_URL}${endpoint}`);
// Never log sensitive endpoints
const sensitiveEndpoints = ['/auth/', '/wallet/'];
const isSensitive = sensitiveEndpoints.some(e => endpoint.includes(e));
if (options.body && !isSensitive) {
console.log('📤 Request body:', options.body);
}
}
🤖 Prompt for AI Agents
In apps/web/src/services/api.ts around lines 175 to 179, the code logs the
request body which may expose sensitive data. Remove the console.log statement
that outputs the request body or modify it to only log in non-production
environments to prevent sensitive data exposure in production.

Comment on lines +26 to +33
CREATE OR REPLACE FUNCTION public.cleanup_expired_challenges()
RETURNS void AS $$
BEGIN
DELETE FROM public.wallet_challenges
WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Schedule automatic cleanup of expired challenges.

The cleanup function is defined but not automatically scheduled. Expired challenges will accumulate without periodic cleanup.

Add a scheduled job using pg_cron or a similar mechanism:

-- After creating the function, schedule it to run every hour
-- This requires pg_cron extension
CREATE EXTENSION IF NOT EXISTS pg_cron;

SELECT cron.schedule(
    'cleanup-expired-wallet-challenges',
    '0 * * * *', -- Run every hour
    'SELECT public.cleanup_expired_challenges();'
);

Alternatively, trigger cleanup on each new challenge creation to maintain table hygiene.

🤖 Prompt for AI Agents
In apps/backend/database/migrations/00004_create_wallet_auth_tables.sql around
lines 26 to 33, the cleanup_expired_challenges function is defined but not
scheduled to run automatically. To fix this, add SQL commands after the function
definition to create the pg_cron extension if not present and schedule the
cleanup_expired_challenges function to run every hour using cron.schedule with
the appropriate cron expression. This will ensure expired challenges are
regularly deleted without manual intervention.

Comment on lines +1 to +13
CREATE TABLE IF NOT EXISTS public.wallet_challenges (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
public_key TEXT NOT NULL,
challenge TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create indexes for efficient lookups
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_public_key ON public.wallet_challenges(public_key);
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_expires_at ON public.wallet_challenges(expires_at);
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_challenge ON public.wallet_challenges(challenge);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add unique constraint to challenge field for security.

The challenge field should have a unique constraint to prevent duplicate challenges from being generated, which could be a security vulnerability.

Add a unique constraint:

 CREATE TABLE IF NOT EXISTS public.wallet_challenges (
     id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
     public_key TEXT NOT NULL,
-    challenge TEXT NOT NULL,
+    challenge TEXT NOT NULL UNIQUE,
     expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
     created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE IF NOT EXISTS public.wallet_challenges (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
public_key TEXT NOT NULL,
challenge TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes for efficient lookups
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_public_key ON public.wallet_challenges(public_key);
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_expires_at ON public.wallet_challenges(expires_at);
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_challenge ON public.wallet_challenges(challenge);
CREATE TABLE IF NOT EXISTS public.wallet_challenges (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
public_key TEXT NOT NULL,
challenge TEXT NOT NULL UNIQUE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes for efficient lookups
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_public_key ON public.wallet_challenges(public_key);
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_expires_at ON public.wallet_challenges(expires_at);
CREATE INDEX IF NOT EXISTS idx_wallet_challenges_challenge ON public.wallet_challenges(challenge);
🤖 Prompt for AI Agents
In apps/backend/database/migrations/00004_create_wallet_auth_tables.sql lines 1
to 13, the challenge column in the wallet_challenges table lacks a unique
constraint, which can lead to duplicate challenges and security issues. Add a
unique constraint on the challenge column by modifying the table definition to
include UNIQUE(challenge) or by adding a separate ALTER TABLE statement to
enforce uniqueness on the challenge field.

Comment on lines +50 to +52
-- Record this migration as completed
INSERT INTO migrations (name) VALUES ('001_create_wallet_auth_tables') ON CONFLICT (name) DO NOTHING;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix migration name to match file name.

The migration name in the INSERT statement doesn't match the actual file name, which could cause migration tracking issues.

--- Record this migration as completed
-INSERT INTO migrations (name) VALUES ('001_create_wallet_auth_tables') ON CONFLICT (name) DO NOTHING;
+INSERT INTO migrations (name) VALUES ('00004_create_wallet_auth_tables') ON CONFLICT (name) DO NOTHING;
🤖 Prompt for AI Agents
In apps/backend/database/migrations/00004_create_wallet_auth_tables.sql around
lines 50 to 52, the migration name in the INSERT statement is
'001_create_wallet_auth_tables' but it should match the file name
'00004_create_wallet_auth_tables'. Update the migration name in the INSERT INTO
migrations statement to '00004_create_wallet_auth_tables' to ensure proper
migration tracking.

Comment on lines 120 to 124
const challengeText = challengeResponse.challenge;
if (challengeText.length > 28) {
throw new Error('Challenge too long for transaction memo');
}
const account = new Account(walletPublicKey, '0');
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix challenge length validation to check UTF-8 byte length.

Stellar text memos support up to 28 bytes in UTF-8, not 28 characters. Multi-byte characters could cause issues.

         const challengeText = challengeResponse.challenge;
-        if (challengeText.length > 28) {
+        const challengeBytes = new TextEncoder().encode(challengeText);
+        if (challengeBytes.length > 28) {
           throw new Error('Challenge too long for transaction memo');
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const challengeText = challengeResponse.challenge;
if (challengeText.length > 28) {
throw new Error('Challenge too long for transaction memo');
}
const account = new Account(walletPublicKey, '0');
const challengeText = challengeResponse.challenge;
const challengeBytes = new TextEncoder().encode(challengeText);
if (challengeBytes.length > 28) {
throw new Error('Challenge too long for transaction memo');
}
const account = new Account(walletPublicKey, '0');
🤖 Prompt for AI Agents
In apps/web/src/hooks/auth/use-auth.tsx around lines 120 to 124, the current
validation checks the challenge length by character count, but Stellar memos
limit to 28 bytes in UTF-8 encoding. Update the validation to calculate the
UTF-8 byte length of challengeResponse.challenge and throw the error if it
exceeds 28 bytes, ensuring multi-byte characters are correctly accounted for.

Copy link
Contributor

@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: 3

🔭 Outside diff range comments (1)
apps/web/src/hooks/auth/procted-route.tsx (1)

1-58: Fix the filename typo to match the expected import path.

The filename procted-route.tsx should be protected-route.tsx (missing 'e') to match the import in the dashboard layout and follow proper naming conventions.

♻️ Duplicate comments (2)
apps/backend/src/services/wallet-auth.service.ts (2)

16-16: Critical: Fix inverted network selection logic.

The network selection is inverted - production should use PUBLIC network, not TESTNET.


56-59: Add error logging in signature verification.

Swallowing errors makes debugging difficult. Log the error before returning false.

🧹 Nitpick comments (5)
apps/web/src/hooks/auth/procted-route.tsx (2)

18-18: Optimize the default prop to prevent unnecessary re-renders.

The allowedAuthTypes default value creates a new array on each render, potentially causing unnecessary re-executions of the useEffect.

+const DEFAULT_ALLOWED_AUTH_TYPES: ('email' | 'wallet')[] = ['email', 'wallet'];
+
 export default function ProtectedRoute({
   children,
   redirectTo = '/login',
-  allowedAuthTypes = ['email', 'wallet'],
+  allowedAuthTypes = DEFAULT_ALLOWED_AUTH_TYPES,
 }: ProtectedRouteProps) {

34-34: Remove or replace the debug console.log statement.

Console logging should be avoided in production code. Consider using a proper logging service or removing this debug statement.

-        console.log('🚫 Auth type not allowed:', authType, 'Allowed:', allowedAuthTypes);
apps/backend/src/services/wallet-auth.service.ts (2)

43-44: Optimize import placement for better performance.

The dynamic import of Keypair inside the function adds unnecessary overhead. Move this to the top-level imports.

-import { Networks, StrKey, Transaction } from '@stellar/stellar-sdk';
+import { Networks, StrKey, Transaction, Keypair } from '@stellar/stellar-sdk';

And update the function:

-    const { Keypair } = await import('@stellar/stellar-sdk');
-    const keypair = Keypair.fromPublicKey(publicKey);
+    const keypair = Keypair.fromPublicKey(publicKey);

151-200: Consider adding request timeout and improved error messages.

The main authentication function lacks timeout handling and could benefit from more specific error messages for different failure scenarios.

Consider adding:

  1. Timeout handling for signature verification
  2. More specific error messages (e.g., "Challenge expired" vs "Invalid challenge")
  3. Structured error responses for better frontend handling
apps/backend/database/migrations/00005_create_profile_table.sql (1)

26-27: Consider more granular RLS policies.

The current policy grants full access to service_role, but more granular policies might be beneficial for security.

Consider adding policies for different access patterns:

-- Allow users to read their own profiles
CREATE POLICY "Users can read own profile" ON public.profiles
    FOR SELECT USING (auth.uid()::text = user_id::text);

-- Allow users to update their own profiles
CREATE POLICY "Users can update own profile" ON public.profiles
    FOR UPDATE USING (auth.uid()::text = user_id::text);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e161cbc and 568d5f5.

📒 Files selected for processing (5)
  • apps/backend/database/migrations/00005_create_profile_table.sql (1 hunks)
  • apps/backend/src/services/wallet-auth.service.ts (1 hunks)
  • apps/web/src/app/dashboard/layout.tsx (1 hunks)
  • apps/web/src/app/dashboard/page.tsx (1 hunks)
  • apps/web/src/hooks/auth/procted-route.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/app/dashboard/page.tsx
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/web/src/app/dashboard/layout.tsx (1)
apps/web/src/hooks/auth/procted-route.tsx (1)
  • ProtectedRoute (15-57)
apps/web/src/hooks/auth/procted-route.tsx (2)
apps/web/src/hooks/auth/use-auth.tsx (1)
  • useAuth (220-220)
apps/web/src/services/authService.ts (1)
  • isAuthenticated (89-108)
🔇 Additional comments (7)
apps/web/src/app/dashboard/layout.tsx (1)

8-14: Clean and appropriate layout implementation.

The dashboard layout properly wraps children in the ProtectedRoute component, providing the necessary authentication protection for dashboard pages.

apps/web/src/hooks/auth/procted-route.tsx (3)

24-39: Well-implemented authentication logic with proper flow control.

The authentication checking logic properly handles the loading states, authentication status, and auth type validation with appropriate redirects.


41-50: Clean loading state implementation.

The loading spinner with proper styling and messaging provides good user feedback during authentication checks.


52-57: Proper guard clause implementation for unauthorized access.

The final authentication check correctly returns null for unauthorized users and renders children for authorized ones.

apps/backend/src/services/wallet-auth.service.ts (1)

98-109: Handle profile creation failure more gracefully.

Profile creation failure only logs a warning but doesn't affect the authentication flow. Consider if this should be a critical error or if the warning approach is intentional.

Should profile creation be mandatory for wallet authentication? The current implementation allows users to authenticate even if profile creation fails, which might lead to inconsistent user experience.

apps/backend/database/migrations/00005_create_profile_table.sql (2)

19-20: Add unique constraint on user_id.

The current schema allows multiple profiles per user, which may not be intended. Consider adding a unique constraint.

Should there be a unique constraint on user_id to ensure one profile per user? The current schema allows multiple profiles per user.

-- If one profile per user is intended:
CREATE UNIQUE INDEX IF NOT EXISTS idx_profiles_user_id_unique ON public.profiles(user_id);

34-46: LGTM: Well-implemented automatic timestamp update.

The trigger function and trigger setup for automatic updated_at timestamp updates is correctly implemented and follows PostgreSQL best practices.

@@ -0,0 +1,16 @@
import type React from 'react';
import ProtectedRoute from '~/hooks/auth/procted-route';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the typo in the import path.

The import path has a typo: procted-route should be protected-route (missing 'e'). This will cause a module not found error.

-import ProtectedRoute from '~/hooks/auth/procted-route';
+import ProtectedRoute from '~/hooks/auth/protected-route';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import ProtectedRoute from '~/hooks/auth/procted-route';
import ProtectedRoute from '~/hooks/auth/protected-route';
🤖 Prompt for AI Agents
In apps/web/src/app/dashboard/layout.tsx at line 2, fix the typo in the import
path by changing 'procted-route' to 'protected-route' to correctly reference the
module and avoid the module not found error.

Comment on lines +118 to +124
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('user_id', userId)
.single();

return profile || undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for database operations.

The getUserProfile function doesn't handle potential database errors, which could cause the authentication flow to fail unexpectedly.

 async function getUserProfile(userId: string) {
-  const { data: profile } = await supabase
+  const { data: profile, error } = await supabase
     .from('profiles')
     .select('*')
     .eq('user_id', userId)
     .single();

+  if (error && error.code !== 'PGRST116') { // PGRST116 is "not found"
+    console.error('Error fetching user profile:', error);
+    throw new Error('Failed to fetch user profile');
+  }
+
   return profile || undefined;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('user_id', userId)
.single();
return profile || undefined;
async function getUserProfile(userId: string) {
const { data: profile, error } = await supabase
.from('profiles')
.select('*')
.eq('user_id', userId)
.single();
if (error && error.code !== 'PGRST116') { // PGRST116 is "not found"
console.error('Error fetching user profile:', error);
throw new Error('Failed to fetch user profile');
}
return profile || undefined;
}
🤖 Prompt for AI Agents
In apps/backend/src/services/wallet-auth.service.ts around lines 118 to 124, the
database query to fetch the user profile lacks error handling, which may cause
unhandled exceptions. Modify the code to check for errors returned by the
Supabase query and handle them appropriately, such as logging the error or
throwing a controlled exception, before returning the profile data or undefined.

Comment on lines +3 to +16
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
name TEXT,
avatar_url TEXT,
phone TEXT,
address TEXT,
preferences JSONB DEFAULT '{}',
social_links JSONB DEFAULT '{}',
verification_status TEXT DEFAULT 'unverified' CHECK (verification_status IN ('verified', 'unverified', 'pending')),
last_active TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding foreign key constraint for user_id.

The user_id column lacks a foreign key constraint, which could lead to orphaned profiles if the referenced user is deleted.

 CREATE TABLE IF NOT EXISTS public.profiles (
     id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
-    user_id UUID NOT NULL,
+    user_id UUID NOT NULL REFERENCES wallet_users(id) ON DELETE CASCADE,
     name TEXT,
     avatar_url TEXT,
     phone TEXT,
     address TEXT,
     preferences JSONB DEFAULT '{}',
     social_links JSONB DEFAULT '{}',
     verification_status TEXT DEFAULT 'unverified' CHECK (verification_status IN ('verified', 'unverified', 'pending')),
     last_active TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
     created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
     updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
name TEXT,
avatar_url TEXT,
phone TEXT,
address TEXT,
preferences JSONB DEFAULT '{}',
social_links JSONB DEFAULT '{}',
verification_status TEXT DEFAULT 'unverified' CHECK (verification_status IN ('verified', 'unverified', 'pending')),
last_active TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES wallet_users(id) ON DELETE CASCADE,
name TEXT,
avatar_url TEXT,
phone TEXT,
address TEXT,
preferences JSONB DEFAULT '{}',
social_links JSONB DEFAULT '{}',
verification_status TEXT DEFAULT 'unverified' CHECK (verification_status IN ('verified', 'unverified', 'pending')),
last_active TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
🤖 Prompt for AI Agents
In apps/backend/database/migrations/00005_create_profile_table.sql around lines
3 to 16, the user_id column is missing a foreign key constraint. Add a foreign
key constraint referencing the users table (or the appropriate users table) on
user_id to ensure referential integrity and prevent orphaned profiles when a
user is deleted.

@respp
Copy link
Contributor

respp commented Jul 3, 2025

#82

Copy link
Contributor

@respp respp left a comment

Choose a reason for hiding this comment

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

The issue requires tests. Once you’ve written them, please place them where they belong inside this folder, in apps/backend/tests

image

Copy link
Contributor

Choose a reason for hiding this comment

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

@Benjtalkshow there are a few things that need improvement. Initially, the backend endpoint and its validation are fine, but there's no 5-minute expiration for the challenge, no timestamp to prevent reuse, and no integration with Redis/Supabase for temporary storage.

Copy link
Contributor

@respp respp left a comment

Choose a reason for hiding this comment

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

I've left most of the feedback directly in the files. Please remember to run the linter and pull the latest changes from the repository before working on the required updates.

Copy link
Contributor

Choose a reason for hiding this comment

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

On the frontend side, new functionality was added that already existed, resulting in duplicated logic. The existing useWallet hook is not used, there is no integration with window.freighterApi.signTransaction, and the signing logic is redundantly reimplemented. Session management is completely missing: there's no secure cookie configuration, no integration with useAuth, no visual feedback like toasts, and no handling of authentication state. Error handling is minimal, cases like "WalletNotFoundError", connection rejections, and expired challenges are not covered, and there are no error messages displayed in the UI. Additionally, no testing has been implemented here either. I suggest revisiting the issue to better address these problems: #82

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On the frontend side, new functionality was added that already existed, resulting in duplicated logic. The existing useWallet hook is not used, there is no integration with window.freighterApi.signTransaction, and the signing logic is redundantly reimplemented. Session management is completely missing: there's no secure cookie configuration, no integration with useAuth, no visual feedback like toasts, and no handling of authentication state. Error handling is minimal, cases like "WalletNotFoundError", connection rejections, and expired challenges are not covered, and there are no error messages displayed in the UI. Additionally, no testing has been implemented here either. I suggest revisiting the issue to better address these problems: #82

@respp
I used the useWallet hook. I chose to use @stellr/frighter-api because window.frighterApi.signTransaction often fails to detect the wallet, I mentioned this in my PR. I also used useAuth and added enough toast messages to show errors.
I’ll fix the issue where expired challenges are not being stored properly. I’ll also write the necessary tests and take care of the other issues you pointed out. Thanks

@Benjtalkshow
Copy link
Contributor Author

The issue requires tests. Once you’ve written them, please place them where they belong inside this folder, in apps/backend/tests

image

@respp
Alright

@Benjtalkshow
Copy link
Contributor Author

@respp
All the files you mentioned that I didn’t use were actually fully utilized and functional. I used the useWallet hook, useAuth, and api.ts. I chose to use @stellar/frighter-api instead of window.frighterApi because it always fails to detect the Frighter wallet.

I even created protected routes for the dashboard since the dashboard wasn’t protected before. The challenge generation and message signing are working perfectly, and I also added appropriate toast messages for every user action.

The only issues I noticed are:

  • The challenge is not being kept for 5 minutes but gets deleted immediately after the message is signed.
  • I forgot to write the test.

Please kindly go through my pull request message, watch the video evidence I attached, or even test it locally. Everything I’ve explained here was already included in my PR description.

Thanks. I’ll write the test and fix the challenge generation issue.

Copy link
Contributor

@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: 3

🔭 Outside diff range comments (1)
apps/backend/package.json (1)

15-15: Critical: Remove duplicate stellar-sdk dependency.

You have two stellar-sdk entries in your dependencies:

  • @stellar/stellar-sdk at line 15
  • stellar-sdk at line 27

This duplication can cause version conflicts and installation issues.

 "@stellar/stellar-sdk": "^13.3.0",
 "@supabase/supabase-js": "^2.39.0",
 "bcrypt": "^5.1.1",
 "cors": "^2.8.5",
 "dotenv": "^16.3.1",
 "express": "^4.18.2",
 "express-rate-limit": "^7.1.5",
 "jest-mock-extended": "^4.0.0",
 "joi": "^17.13.3",
 "jsonwebtoken": "^9.0.2",
 "node-cron": "^4.2.0",
 "soroban-client": "^1.0.1",
-"stellar-sdk": "^13.3.0",
 "supertest": "^7.1.2",
 "uuid": "^11.1.0",
 "zod": "^3.22.4"

Also applies to: 27-27

♻️ Duplicate comments (4)
apps/backend/package.json (1)

23-23: Question the need for Joi alongside Zod.

You're adding joi validation library while already using zod (line 30). This introduces inconsistency in validation approaches across the codebase.

Consider removing Joi and using Zod consistently throughout the application:

-    "joi": "^17.13.3",

If Joi is truly needed for specific use cases, please document why both validation libraries are necessary.

apps/backend/src/services/wallet-challenge.service.ts (1)

75-81: Consider throwing an error when challenge removal fails.

Failed challenge removal could allow challenge reuse, which is a security concern.

 export async function removeChallenge(challengeId: string): Promise<void> {
   const { error } = await supabase.from('wallet_challenges').delete().eq('id', challengeId);

   if (error) {
     console.error('Error removing challenge:', error);
+    throw new Error('Failed to remove challenge');
   }
 }
apps/backend/src/services/wallet-auth.service.ts (1)

131-137: Add error handling for database operations.

The getUserProfile function doesn't handle potential database errors, which could cause the authentication flow to fail unexpectedly.

 async function getUserProfile(userId: string) {
-  const { data: profile } = await supabase
+  const { data: profile, error } = await supabase
     .from('profiles')
     .select('*')
     .eq('user_id', userId)
     .single();

+  if (error && error.code !== 'PGRST116') { // PGRST116 is "not found"
+    console.error('Error fetching user profile:', error);
+    throw new Error('Failed to fetch user profile');
+  }
+
   return profile || undefined;
 }
apps/web/src/services/api.ts (1)

173-177: Remove request body logging to prevent sensitive data exposure.

This security issue was already identified in a previous review. Request body logging can expose sensitive information like passwords, authentication tokens, or private keys.

🧹 Nitpick comments (1)
apps/web/src/components/NetworkStatus.tsx (1)

6-68: Well-structured component with good UX patterns.

The component implements proper conditional rendering, comprehensive network mapping, and semantic color coding. The early returns for loading and disconnected states provide good UX.

Consider memoizing the network info calculation for better performance:

+import { useMemo } from 'react';
 import { AlertTriangle, CheckCircle, Globe } from 'lucide-react';
 import { useWallet } from '../hooks/useWallet';

 export default function NetworkStatus() {
   const { network, isConnected, isLoading } = useWallet();

   if (isLoading || !isConnected) {
     return null;
   }

-  const getNetworkInfo = () => {
+  const networkInfo = useMemo(() => {
     switch (network?.toUpperCase()) {
       // ... existing cases
     }
-  };
+  }, [network]);

-  const networkInfo = getNetworkInfo();
   const IconComponent = networkInfo.icon;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd338a0 and 31de752.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • apps/backend/package.json (1 hunks)
  • apps/backend/src/controllers/wallet-auth.controller.ts (1 hunks)
  • apps/backend/src/index.ts (3 hunks)
  • apps/backend/src/services/cleanup-schedular.ts (1 hunks)
  • apps/backend/src/services/wallet-auth.service.ts (1 hunks)
  • apps/backend/src/services/wallet-challenge.service.ts (1 hunks)
  • apps/backend/src/types/wallet-error-types.ts (1 hunks)
  • apps/backend/tests/integration/wallet-auth.test.ts (1 hunks)
  • apps/web/src/components/NetworkStatus.tsx (1 hunks)
  • apps/web/src/components/auth/WalletAuthButton.tsx (1 hunks)
  • apps/web/src/hooks/auth/use-auth.tsx (3 hunks)
  • apps/web/src/hooks/useWallet.ts (1 hunks)
  • apps/web/src/lib/network-utils.ts (1 hunks)
  • apps/web/src/services/api.ts (13 hunks)
✅ Files skipped from review due to trivial changes (2)
  • apps/backend/src/types/wallet-error-types.ts
  • apps/web/src/lib/network-utils.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/backend/src/index.ts
  • apps/backend/src/controllers/wallet-auth.controller.ts
  • apps/web/src/hooks/auth/use-auth.tsx
  • apps/web/src/hooks/useWallet.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/backend/src/services/cleanup-schedular.ts (1)
apps/backend/src/services/wallet-challenge.service.ts (1)
  • cleanupAllExpiredChallenges (109-139)
apps/web/src/components/NetworkStatus.tsx (1)
apps/web/src/hooks/useWallet.ts (1)
  • useWallet (23-211)
apps/backend/tests/integration/wallet-auth.test.ts (6)
apps/backend/src/config/supabase.ts (1)
  • supabase (205-217)
apps/backend/src/controllers/wallet-auth.controller.ts (1)
  • generateChallenge (16-27)
apps/backend/src/services/wallet-challenge.service.ts (4)
  • generateChallenge (8-48)
  • getValidChallenge (53-70)
  • removeChallenge (75-81)
  • cleanupAllExpiredChallenges (109-139)
apps/backend/src/services/wallet-auth.service.ts (1)
  • authenticateWallet (164-211)
apps/backend/src/types/wallet-error-types.ts (5)
  • InvalidPublicKeyError (29-34)
  • InvalidChallengeError (15-20)
  • SignatureVerificationError (22-27)
  • WalletNotFoundError (1-6)
  • ConnectionRejectedError (8-13)
apps/backend/src/index.ts (1)
  • app (24-24)
🔇 Additional comments (6)
apps/backend/package.json (1)

25-25: Dependencies serve clear purposes for wallet authentication.

The added dependencies are well-justified:

  • node-cron for scheduled cleanup tasks
  • supertest for API testing
  • uuid for unique identifier generation

These align with the wallet authentication features being implemented.

Also applies to: 28-29

apps/backend/src/services/wallet-challenge.service.ts (1)

1-48: Solid implementation with good security practices.

The service properly handles challenge lifecycle with appropriate expiry times, validation, and cleanup. The reuse of valid challenges is a good optimization, and the logging provides good operational visibility.

Also applies to: 53-70, 109-139

apps/backend/src/services/wallet-auth.service.ts (1)

15-74: Comprehensive wallet authentication implementation.

The service implements a robust authentication flow with proper transaction verification, user management, and JWT generation. The signature verification logic is solid, and the error handling and logging throughout most of the service is excellent.

Also applies to: 78-125, 143-211

apps/web/src/components/auth/WalletAuthButton.tsx (1)

54-74: Well-structured error handling!

The error message mapping provides clear, user-friendly messages for different error scenarios. Good defensive programming with the fallback for unknown error types.

apps/backend/tests/integration/wallet-auth.test.ts (1)

1-447: Excellent test coverage!

This is a comprehensive test suite that covers all aspects of the wallet authentication system:

  • Challenge generation and lifecycle
  • Authentication flow with valid/invalid inputs
  • API endpoints with proper error handling
  • Custom error types

The console suppression pattern (lines 35-46) is a nice touch to keep test output clean.

apps/web/src/services/api.ts (1)

424-461: Well-implemented wallet authentication methods!

The new wallet authentication methods follow the existing API patterns and properly handle errors. The challenge-response flow is correctly implemented with appropriate error handling.

Comment on lines +1 to +37
import cron, { type ScheduledTask } from 'node-cron';
import { cleanupAllExpiredChallenges } from './wallet-challenge.service';

let cleanupJob: ScheduledTask | null = null;
export function startCleanupScheduler() {
// Run cleanup every 2 minutes
cleanupJob = cron.schedule('*/2 * * * *', async () => {
try {
console.log('Running expired challenges cleanup...');
await cleanupAllExpiredChallenges();
console.log('Expired challenges cleanup completed');
} catch (error) {
console.error('Error during challenges cleanup:', error);
}
});

console.log('Challenge cleanup scheduler started (runs every 2 minutes)');
}

export function stopCleanupScheduler() {
if (cleanupJob) {
cleanupJob.stop();
cleanupJob = null;
console.log('Challenge cleanup scheduler stopped');
}
}

// Run initial cleanup on startup
export async function runInitialCleanup() {
try {
console.log('Running initial expired challenges cleanup...');
await cleanupAllExpiredChallenges();
console.log('Initial cleanup completed');
} catch (error) {
console.error('Error during initial cleanup:', error);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix filename typo and consider configurable cleanup interval.

The implementation logic is solid with proper error handling and logging. However, there's a typo in the filename: "schedular" should be "scheduler".

-// File: apps/backend/src/services/cleanup-schedular.ts
+// File: apps/backend/src/services/cleanup-scheduler.ts

Also consider making the cleanup interval configurable via environment variables:

export function startCleanupScheduler() {
+  const cleanupInterval = process.env.CLEANUP_INTERVAL || '*/2 * * * *';
-  cleanupJob = cron.schedule('*/2 * * * *', async () => {
+  cleanupJob = cron.schedule(cleanupInterval, async () => {
    // ... rest of the function
  });
- console.log('Challenge cleanup scheduler started (runs every 2 minutes)');
+ console.log(`Challenge cleanup scheduler started (runs every ${cleanupInterval})`);
}

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

🤖 Prompt for AI Agents
In apps/backend/src/services/cleanup-schedular.ts lines 1 to 37, rename the file
to cleanup-scheduler.ts to fix the typo. Additionally, modify the cron schedule
expression to be configurable by reading the interval from an environment
variable, with a default fallback to the current 2-minute interval. This allows
flexible adjustment of the cleanup frequency without code changes.

Comment on lines +33 to +48
const handleAuthenticate = async () => {
setAuthState('authenticating');
try {
await loginWithWallet();
setAuthState('success');
onSuccess?.();
} catch (error) {
console.error('Authentication failed:', error);
const errorMessage = getErrorMessage(error);
onError?.(errorMessage);
setAuthState('error');

// Reset to idle after showing error briefly
setTimeout(() => setAuthState('idle'), 3000);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add cleanup for setTimeout to prevent memory leaks.

The setTimeout on line 46 could cause a memory leak if the component unmounts before the timeout completes. Additionally, logging the full error object might expose sensitive information in production.

Apply this diff to fix the issues:

+import { useCallback, useEffect, useRef } from 'react';

 const handleAuthenticate = async () => {
+  const timeoutRef = useRef<NodeJS.Timeout>();
   setAuthState('authenticating');
   try {
     await loginWithWallet();
     setAuthState('success');
     onSuccess?.();
   } catch (error) {
-    console.error('Authentication failed:', error);
+    if (process.env.NODE_ENV === 'development') {
+      console.error('Authentication failed:', error);
+    }
     const errorMessage = getErrorMessage(error);
     onError?.(errorMessage);
     setAuthState('error');

     // Reset to idle after showing error briefly
-    setTimeout(() => setAuthState('idle'), 3000);
+    timeoutRef.current = setTimeout(() => setAuthState('idle'), 3000);
   }
 };
+
+// Add cleanup effect
+useEffect(() => {
+  return () => {
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+    }
+  };
+}, []);

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

🤖 Prompt for AI Agents
In apps/web/src/components/auth/WalletAuthButton.tsx around lines 33 to 48, the
setTimeout used to reset auth state after an error can cause a memory leak if
the component unmounts before it fires. To fix this, store the timeout ID in a
ref and clear it in a useEffect cleanup function. Also, replace console.error
with a safer logging method that avoids exposing sensitive error details in
production, such as logging only the error message or a sanitized version.

Comment on lines +127 to +152
// ===========================
// Transform wallet user to UserProfile
// ===========================
function transformWalletUser(walletUser: WalletAuthResponse['user']): UserProfile {
return {
id: walletUser.id,
name: walletUser.profile?.name || 'Wallet User',
email: '',
phone: walletUser.profile?.phone || '',
avatar:
walletUser.profile?.avatar_url ||
'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100',
memberSince: walletUser.profile?.last_active
? new Date(walletUser.profile.last_active).getFullYear().toString()
: new Date().getFullYear().toString(),
verified: walletUser.profile?.verification_status === 'verified',
location: walletUser.profile?.address,
bio: '',
preferences: {
currency: 'USD',
language: 'English',
notifications: true,
},
publicKey: walletUser.publicKey,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider handling the empty email field for wallet users.

The transformWalletUser function sets an empty string for the email field (line 134). This might cause issues in parts of the application that expect a valid email address.

Consider either:

  1. Using a placeholder email format like wallet-${publicKey.substring(0, 8)}@stellar.local
  2. Making the email field optional in the UserProfile type
  3. Documenting this behavior for wallet-authenticated users

🏁 Script executed:

#!/bin/bash
# Description: Check if any code expects non-empty email values from UserProfile

# Search for email field usage in the codebase
rg -A 3 -B 3 "\.email\b" --type ts --type tsx | grep -v "test\." | head -20

# Check for email validation or required email checks
ast-grep --pattern 'if ($_.email) { $$$ }'

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Search for any usage of the email property on UserProfile across the repo
rg -n "\.email\b"

# Locate the definition of UserProfile to inspect whether email is required or optional
rg -n "interface UserProfile" -A3 -B1 || echo "No interface UserProfile found"
rg -n "type UserProfile" -A3 -B1 || echo "No type alias UserProfile found"

Length of output: 2259


Ensure wallet users have a valid or optional email
Components across the app (e.g. profile‐management, HostContactCard, booking mailto links) assume UserProfile.email is nonempty and valid. Returning '' will lead to broken mailto URLs, blank UI fields, or regex validation failures.

Changes required:

  • In apps/web/src/services/api.ts (transformWalletUser), replace the empty string with either a placeholder or undefined
  • In apps/web/src/types/index.ts, update UserProfile.email to email?: string if you choose to allow missing emails
  • Guard UI code (tenant‐dashboard, HostContactCard, etc.) to skip or disable email‐based flows when there is no email

Suggested diff for placeholder email approach:

--- a/apps/web/src/services/api.ts
@@ function transformWalletUser(walletUser: WalletAuthResponse['user']): UserProfile {
-   email: '',
+   email:
+     walletUser.profile?.email ||
+     `wallet-${walletUser.publicKey.substring(0, 8)}@stellar.local`,

And in apps/web/src/types/index.ts:

 export interface UserProfile {
   id: string;
   name: string;
-  email: string;
+  email?: string;
   // …
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ===========================
// Transform wallet user to UserProfile
// ===========================
function transformWalletUser(walletUser: WalletAuthResponse['user']): UserProfile {
return {
id: walletUser.id,
name: walletUser.profile?.name || 'Wallet User',
email: '',
phone: walletUser.profile?.phone || '',
avatar:
walletUser.profile?.avatar_url ||
'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100',
memberSince: walletUser.profile?.last_active
? new Date(walletUser.profile.last_active).getFullYear().toString()
: new Date().getFullYear().toString(),
verified: walletUser.profile?.verification_status === 'verified',
location: walletUser.profile?.address,
bio: '',
preferences: {
currency: 'USD',
language: 'English',
notifications: true,
},
publicKey: walletUser.publicKey,
};
}
// ===========================
// Transform wallet user to UserProfile
// ===========================
function transformWalletUser(walletUser: WalletAuthResponse['user']): UserProfile {
return {
id: walletUser.id,
name: walletUser.profile?.name || 'Wallet User',
email:
walletUser.profile?.email ||
`wallet-${walletUser.publicKey.substring(0, 8)}@stellar.local`,
phone: walletUser.profile?.phone || '',
avatar:
walletUser.profile?.avatar_url ||
'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100',
memberSince: walletUser.profile?.last_active
? new Date(walletUser.profile.last_active).getFullYear().toString()
: new Date().getFullYear().toString(),
verified: walletUser.profile?.verification_status === 'verified',
location: walletUser.profile?.address,
bio: '',
preferences: {
currency: 'USD',
language: 'English',
notifications: true,
},
publicKey: walletUser.publicKey,
};
}
🤖 Prompt for AI Agents
In apps/web/src/services/api.ts between lines 127 and 152, the
transformWalletUser function returns an empty string for the email field, which
causes issues in components expecting a valid nonempty email. To fix this,
replace the empty string with either a placeholder email or undefined.
Additionally, update the UserProfile.email type in apps/web/src/types/index.ts
to be optional (email?: string) if allowing missing emails. Finally, review and
update UI components like tenant-dashboard and HostContactCard to handle cases
where email is missing by skipping or disabling email-dependent features.

@Benjtalkshow
Copy link
Contributor Author

@respp
Here are the changes I made:

  • Challenges are stored in Supabase with a 5-minute expiration, and a cron job cleans up expired challenges.
  • I wrote tests in the appropriate files. See the screenshot below.
  • In the frontend, the JWT is stored in cookies from the backend, and minor user details are stored in localStorage.
  • I avoided using window.freighterApi because it doesn’t reliably detect the wallet. That’s why I didn’t use stellar.ts in the frontend, but I used other necessary files.
  • Implemented Single-Flow Authentication.
  • Standardized Network Detection.
  • Fixed Transaction Structure.
  • Improved Smart Wallet Connection.

image

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.

Secure wallet-based

2 participants