Files
tabungin/RESEND_OTP_FIX.md
dwindown 249f3a9d7d feat: remove OTP gate from transactions, fix categories auth, add implementation plan
- Remove OtpGateGuard from transactions controller (OTP verified at login)
- Fix categories controller to use authenticated user instead of TEMP_USER_ID
- Add comprehensive implementation plan document
- Update .env.example with WEB_APP_URL
- Prepare for admin dashboard development
2025-10-11 14:00:11 +07:00

3.0 KiB

🔧 Resend OTP Fix - Public Endpoint

🐛 Problem:

Resend OTP endpoint was failing with ERR_CONNECTION_REFUSED because:

  1. The entire OtpController has @UseGuards(AuthGuard) at class level
  2. The resend endpoint requires a temp token, not a full JWT
  3. AuthGuard was rejecting the request

Solution:

Made the resend endpoint public by:

  1. Adding @Public() decorator
  2. Updating AuthGuard to respect public routes
  3. Endpoint verifies temp token manually

📝 Changes Made:

1. Updated OtpController (otp.controller.ts):

// Added Public decorator
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// Marked resend endpoint as public
@Public()
@Post('email/resend')
async resendEmailOtp(@Body() body: { tempToken: string }) {
  try {
    // Verify temp token manually
    const payload = this.jwtService.verify(body.tempToken);
    
    if (!payload.temp) {
      throw new UnauthorizedException('Invalid token type');
    }

    const userId = payload.userId || payload.sub;
    
    // Send OTP
    return this.otpService.sendEmailOtp(userId);
  } catch (error) {
    throw new UnauthorizedException('Invalid or expired token');
  }
}

2. Updated AuthGuard (auth.guard.ts):

import { Reflector } from '@nestjs/core';

@Injectable()
export class AuthGuard extends PassportAuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    // Check if route is marked as public
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (isPublic) {
      return true; // Skip authentication
    }
    
    return super.canActivate(context);
  }
}

🔄 How It Works:

  1. Frontend calls /api/otp/email/resend with temp token
  2. AuthGuard checks for @Public() decorator
  3. If public: Skips JWT validation, allows request through
  4. Endpoint manually verifies temp token
  5. Extracts userId from temp token
  6. Sends OTP email
  7. Returns success

🧪 Testing:

Test Resend:

  1. Login with email OTP enabled
  2. On OTP page, wait 30 seconds
  3. Click "Resend Code"
  4. Should work now!
  5. Check console for new OTP code
  6. Enter code and login

⚠️ Current Status:

Backend needs to restart to apply changes. If backend is not responding:

# Kill existing process
pkill -f "nest start"

# Restart
cd apps/api
npm run dev

📊 Files Modified:

  1. apps/api/src/otp/otp.controller.ts

    • Added Public decorator
    • Marked email/resend as public
    • Injected JwtService
  2. apps/api/src/auth/auth.guard.ts

    • Injected Reflector
    • Added public route check
    • Skip auth for public routes
  3. apps/web/src/components/pages/OtpVerification.tsx

    • Already updated to use /api/otp/email/resend

Once backend restarts, resend should work! 🚀