87 lines
2.5 KiB
Ruby
87 lines
2.5 KiB
Ruby
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
|