completed SMS gateway project

This commit is contained in:
Min Zeya Phyo
2025-10-22 17:22:17 +08:00
commit c883fa7128
190 changed files with 16294 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
module Admin
class ApiKeysController < BaseController
def index
@api_keys = ApiKey.order(created_at: :desc)
end
def new
@api_key = ApiKey.new
end
def create
# Build permissions hash
permissions = {}
permissions["send_sms"] = params.dig(:api_key, :send_sms) == "1"
permissions["receive_sms"] = params.dig(:api_key, :receive_sms) == "1"
permissions["manage_gateways"] = params.dig(:api_key, :manage_gateways) == "1"
permissions["manage_otp"] = params.dig(:api_key, :manage_otp) == "1"
# Parse expiration date if provided
expires_at = if params.dig(:api_key, :expires_at).present?
Time.parse(params[:api_key][:expires_at])
else
nil
end
# Generate API key
result = ApiKey.generate!(
name: params[:api_key][:name],
permissions: permissions,
expires_at: expires_at
)
# Store in session to pass to show action
session[:new_api_key_id] = result[:api_key].id
session[:new_api_raw_key] = result[:raw_key]
redirect_to admin_api_key_path(result[:api_key])
rescue StandardError => e
Rails.logger.error "API Key creation failed: #{e.message}\n#{e.backtrace.join("\n")}"
flash.now[:alert] = "Error creating API key: #{e.message}"
@api_key = ApiKey.new(name: params.dig(:api_key, :name))
render :new, status: :unprocessable_entity
end
def show
@api_key = ApiKey.find(params[:id])
# Check if this is a newly created key (from session)
if session[:new_api_key_id] == @api_key.id && session[:new_api_raw_key].present?
@raw_key = session[:new_api_raw_key]
# Clear session data after retrieving
session.delete(:new_api_key_id)
session.delete(:new_api_raw_key)
else
# This is an existing key being viewed (shouldn't normally happen)
redirect_to admin_api_keys_path, alert: "Cannot view API key details after creation"
end
end
def destroy
@api_key = ApiKey.find(params[:id])
@api_key.revoke!
redirect_to admin_api_keys_path, notice: "API key revoked successfully"
rescue => e
redirect_to admin_api_keys_path, alert: "Error revoking API key: #{e.message}"
end
def toggle
@api_key = ApiKey.find(params[:id])
@api_key.update!(active: !@api_key.active)
redirect_to admin_api_keys_path, notice: "API key #{@api_key.active? ? 'activated' : 'deactivated'}"
rescue => e
redirect_to admin_api_keys_path, alert: "Error updating API key: #{e.message}"
end
end
end

View File

@@ -0,0 +1,8 @@
module Admin
class ApiTesterController < BaseController
def index
@api_keys = ApiKey.active_keys.order(created_at: :desc)
@gateways = Gateway.order(created_at: :desc)
end
end
end

View File

@@ -0,0 +1,30 @@
module Admin
class BaseController < ActionController::Base
include Pagy::Backend
# Enable session and flash for admin controllers
# (needed because the app is in API-only mode)
protect_from_forgery with: :exception
layout "admin"
before_action :require_admin
private
def current_admin
@current_admin ||= AdminUser.find_by(id: session[:admin_id]) if session[:admin_id]
end
helper_method :current_admin
def logged_in?
current_admin.present?
end
helper_method :logged_in?
def require_admin
unless logged_in?
redirect_to admin_login_path, alert: "Please log in to continue"
end
end
end
end

View File

@@ -0,0 +1,20 @@
module Admin
class DashboardController < BaseController
def index
@stats = {
total_gateways: Gateway.count,
online_gateways: Gateway.online.count,
total_api_keys: ApiKey.count,
active_api_keys: ApiKey.active_keys.count,
messages_today: SmsMessage.where("created_at >= ?", Time.current.beginning_of_day).count,
messages_sent_today: SmsMessage.where("created_at >= ? AND direction = ?", Time.current.beginning_of_day, "outbound").count,
messages_received_today: SmsMessage.where("created_at >= ? AND direction = ?", Time.current.beginning_of_day, "inbound").count,
failed_messages_today: SmsMessage.where("created_at >= ? AND status = ?", Time.current.beginning_of_day, "failed").count,
pending_messages: SmsMessage.pending.count
}
@recent_messages = SmsMessage.order(created_at: :desc).limit(10)
@recent_gateways = Gateway.order(last_heartbeat_at: :desc).limit(5)
end
end
end

View File

@@ -0,0 +1,152 @@
module Admin
class GatewaysController < BaseController
def index
@gateways = Gateway.order(created_at: :desc)
end
def new
@gateway = Gateway.new
end
def create
@gateway = Gateway.new(
device_id: params[:gateway][:device_id],
name: params[:gateway][:name],
priority: params[:gateway][:priority] || 1,
status: "offline"
)
# Generate API key for the gateway
raw_key = @gateway.generate_api_key!
# Store in session to pass to show action
session[:new_gateway_id] = @gateway.id
session[:new_gateway_raw_key] = raw_key
redirect_to admin_gateway_path(@gateway)
rescue StandardError => e
Rails.logger.error "Gateway creation failed: #{e.message}\n#{e.backtrace.join("\n")}"
flash.now[:alert] = "Error creating gateway: #{e.message}"
@gateway ||= Gateway.new
render :new, status: :unprocessable_entity
end
def show
@gateway = Gateway.find(params[:id])
# Check if this is a newly created gateway (from session)
if session[:new_gateway_id] == @gateway.id && session[:new_gateway_raw_key].present?
@raw_key = session[:new_gateway_raw_key]
@is_new = true
# Generate QR code with configuration data
@qr_code_data = generate_qr_code_data(@raw_key)
# Clear session data after retrieving
session.delete(:new_gateway_id)
session.delete(:new_gateway_raw_key)
else
@is_new = false
@recent_messages = SmsMessage.where(gateway_id: @gateway.id).order(created_at: :desc).limit(20)
end
end
private
def generate_qr_code_data(api_key)
require "rqrcode"
# Determine the base URL and WebSocket URL
base_url = request.base_url
ws_url = request.base_url.sub(/^http/, "ws") + "/cable"
# Create JSON configuration for the Android app
config_data = {
api_key: api_key,
api_base_url: base_url,
websocket_url: ws_url,
version: "1.0"
}.to_json
# Generate QR code
qr = RQRCode::QRCode.new(config_data, level: :h)
# Return as SVG string
qr.as_svg(
offset: 0,
color: "000",
shape_rendering: "crispEdges",
module_size: 4,
standalone: true,
use_path: true
)
end
def toggle
@gateway = Gateway.find(params[:id])
@gateway.update!(active: !@gateway.active)
redirect_to admin_gateways_path, notice: "Gateway #{@gateway.active? ? 'activated' : 'deactivated'}"
rescue => e
redirect_to admin_gateways_path, alert: "Error updating gateway: #{e.message}"
end
def test
@gateway = Gateway.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to admin_gateways_path, alert: "Gateway not found"
end
def check_connection
@gateway = Gateway.find(params[:id])
# Check if gateway is online based on recent heartbeat
if @gateway.online?
render json: {
status: "success",
message: "Gateway is online",
last_heartbeat: @gateway.last_heartbeat_at,
time_ago: helpers.time_ago_in_words(@gateway.last_heartbeat_at)
}
else
render json: {
status: "error",
message: "Gateway is offline",
last_heartbeat: @gateway.last_heartbeat_at,
time_ago: @gateway.last_heartbeat_at ? helpers.time_ago_in_words(@gateway.last_heartbeat_at) : "never"
}
end
rescue StandardError => e
render json: { status: "error", message: e.message }, status: :internal_server_error
end
def send_test_sms
@gateway = Gateway.find(params[:id])
phone_number = params[:phone_number]
message_body = params[:message_body]
if phone_number.blank? || message_body.blank?
render json: { status: "error", message: "Phone number and message are required" }, status: :unprocessable_entity
return
end
# Create test SMS message
sms = SmsMessage.create!(
direction: "outbound",
phone_number: phone_number,
message_body: message_body,
gateway: @gateway,
metadata: { test: true, sent_from: "admin_interface" }
)
render json: {
status: "success",
message: "Test SMS queued for sending",
message_id: sms.message_id,
sms_status: sms.status
}
rescue StandardError => e
Rails.logger.error "Test SMS failed: #{e.message}\n#{e.backtrace.join("\n")}"
render json: { status: "error", message: e.message }, status: :internal_server_error
end
end
end

View File

@@ -0,0 +1,37 @@
module Admin
class LogsController < BaseController
def index
@pagy, @messages = pagy(
apply_filters(SmsMessage).order(created_at: :desc),
items: 50
)
respond_to do |format|
format.html
format.turbo_stream
end
end
private
def apply_filters(scope)
scope = scope.where(direction: params[:direction]) if params[:direction].present?
scope = scope.where(status: params[:status]) if params[:status].present?
scope = scope.where(gateway_id: params[:gateway_id]) if params[:gateway_id].present?
if params[:phone_number].present?
scope = scope.where("phone_number LIKE ?", "%#{params[:phone_number]}%")
end
if params[:start_date].present?
scope = scope.where("created_at >= ?", Time.parse(params[:start_date]))
end
if params[:end_date].present?
scope = scope.where("created_at <= ?", Time.parse(params[:end_date]).end_of_day)
end
scope
end
end
end

View File

@@ -0,0 +1,38 @@
module Admin
class SessionsController < ActionController::Base
layout "admin"
# CSRF protection is enabled by default in ActionController::Base
# We need it for the create action but not for the new (GET) action
protect_from_forgery with: :exception
def new
redirect_to admin_dashboard_path if current_admin
end
def create
admin = AdminUser.find_by(email: params[:email]&.downcase)
if admin&.authenticate(params[:password])
session[:admin_id] = admin.id
admin.update_last_login!
redirect_to admin_dashboard_path, notice: "Welcome back, #{admin.name}!"
else
flash.now[:alert] = "Invalid email or password"
render :new, status: :unprocessable_entity
end
end
def destroy
session.delete(:admin_id)
redirect_to admin_login_path, notice: "You have been logged out"
end
private
def current_admin
@current_admin ||= AdminUser.find_by(id: session[:admin_id]) if session[:admin_id]
end
helper_method :current_admin
end
end