Files
MySMSAPio/app/controllers/api/v1/otp_controller.rb
2025-10-22 17:22:17 +08:00

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