module Api module V1 class OtpController < ApplicationController include ApiAuthenticatable include RateLimitable # POST /api/v1/otp/send def send_otp phone_number = params.require(:phone_number) purpose = params[:purpose] || "authentication" expiry_minutes = params[:expiry_minutes]&.to_i || 5 # Rate limit by phone number return unless rate_limit_by_phone!(phone_number, limit: 3, period: 1.hour) # Validate phone number phone = Phonelib.parse(phone_number) unless phone.valid? render json: { error: "Invalid phone number format" }, status: :unprocessable_entity return end # Send OTP result = OtpCode.send_otp( phone.e164, purpose: purpose, expiry_minutes: expiry_minutes, ip_address: request.remote_ip ) render json: { success: true, expires_at: result[:otp].expires_at, message_id: result[:sms].message_id } rescue ActiveRecord::RecordInvalid => e # Rate limit error from OTP model if e.record.errors[:base].any? render json: { error: e.record.errors[:base].first }, status: :too_many_requests else render json: { error: e.message, details: e.record.errors.full_messages }, status: :unprocessable_entity end rescue ActionController::ParameterMissing => e render json: { error: e.message }, status: :bad_request end # POST /api/v1/otp/verify def verify phone_number = params.require(:phone_number) code = params.require(:code) # Validate phone number phone = Phonelib.parse(phone_number) unless phone.valid? render json: { error: "Invalid phone number format" }, status: :unprocessable_entity return end # Verify OTP result = OtpCode.verify(phone.e164, code) if result[:success] render json: { success: true, verified: true } else attempts_remaining = 3 - (result[:attempts_remaining] || 0) render json: { success: false, verified: false, error: result[:error], attempts_remaining: [attempts_remaining, 0].max } end rescue ActionController::ParameterMissing => e render json: { error: e.message }, status: :bad_request end end end end