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

11
db/cable_schema.rb Normal file
View File

@@ -0,0 +1,11 @@
ActiveRecord::Schema[7.1].define(version: 1) do
create_table "solid_cable_messages", force: :cascade do |t|
t.binary "channel", limit: 1024, null: false
t.binary "payload", limit: 536870912, null: false
t.datetime "created_at", null: false
t.integer "channel_hash", limit: 8, null: false
t.index ["channel"], name: "index_solid_cable_messages_on_channel"
t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
end
end

14
db/cache_schema.rb Normal file
View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
ActiveRecord::Schema[7.2].define(version: 1) do
create_table "solid_cache_entries", force: :cascade do |t|
t.binary "key", limit: 1024, null: false
t.binary "value", limit: 536870912, null: false
t.datetime "created_at", null: false
t.integer "key_hash", limit: 8, null: false
t.integer "byte_size", limit: 4, null: false
t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
end
end

View File

@@ -0,0 +1,24 @@
class CreateGateways < ActiveRecord::Migration[8.0]
def change
create_table :gateways do |t|
t.string :device_id, null: false
t.string :name
t.string :api_key_digest, null: false
t.string :status, default: "offline"
t.datetime :last_heartbeat_at
t.integer :messages_sent_today, default: 0
t.integer :messages_received_today, default: 0
t.integer :total_messages_sent, default: 0
t.integer :total_messages_received, default: 0
t.boolean :active, default: true
t.integer :priority, default: 1
t.jsonb :metadata, default: {}
t.timestamps
end
add_index :gateways, :device_id, unique: true
add_index :gateways, :status
add_index :gateways, :active
end
end

View File

@@ -0,0 +1,26 @@
class CreateSmsMessages < ActiveRecord::Migration[8.0]
def change
create_table :sms_messages do |t|
t.references :gateway, foreign_key: true
t.string :message_id, null: false
t.string :direction, null: false
t.string :phone_number, null: false
t.text :message_body, null: false
t.string :status, default: "pending"
t.text :error_message
t.integer :retry_count, default: 0
t.datetime :sent_at
t.datetime :delivered_at
t.datetime :failed_at
t.jsonb :metadata, default: {}
t.timestamps
end
add_index :sms_messages, :message_id, unique: true
add_index :sms_messages, :phone_number
add_index :sms_messages, [:phone_number, :created_at]
add_index :sms_messages, [:status, :created_at]
add_index :sms_messages, :direction
end
end

View File

@@ -0,0 +1,21 @@
class CreateOtpCodes < ActiveRecord::Migration[8.0]
def change
create_table :otp_codes do |t|
t.string :phone_number, null: false
t.string :code, null: false
t.string :purpose, default: "authentication"
t.datetime :expires_at, null: false
t.boolean :verified, default: false
t.datetime :verified_at
t.integer :attempts, default: 0
t.string :ip_address
t.jsonb :metadata, default: {}
t.timestamps
end
add_index :otp_codes, :phone_number
add_index :otp_codes, :expires_at
add_index :otp_codes, [:phone_number, :verified, :expires_at]
end
end

View File

@@ -0,0 +1,18 @@
class CreateWebhookConfigs < ActiveRecord::Migration[8.0]
def change
create_table :webhook_configs do |t|
t.string :name, null: false
t.string :url, null: false
t.string :event_type, null: false
t.string :secret_key
t.boolean :active, default: true
t.integer :timeout, default: 30
t.integer :retry_count, default: 3
t.timestamps
end
add_index :webhook_configs, :event_type
add_index :webhook_configs, :active
end
end

View File

@@ -0,0 +1,19 @@
class CreateApiKeys < ActiveRecord::Migration[8.0]
def change
create_table :api_keys do |t|
t.string :name, null: false
t.string :key_digest, null: false
t.string :key_prefix, null: false
t.boolean :active, default: true
t.datetime :last_used_at
t.datetime :expires_at
t.jsonb :permissions, default: {}
t.timestamps
end
add_index :api_keys, :key_digest, unique: true
add_index :api_keys, :key_prefix
add_index :api_keys, :active
end
end

View File

@@ -0,0 +1,14 @@
class CreateAdmins < ActiveRecord::Migration[8.0]
def change
create_table :admins do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.string :name, null: false
t.datetime :last_login_at
t.timestamps
end
add_index :admins, :email, unique: true
end
end

View File

@@ -0,0 +1,5 @@
class RenameAdminsToAdminUsers < ActiveRecord::Migration[8.0]
def change
rename_table :admins, :admin_users
end
end

129
db/queue_schema.rb Normal file
View File

@@ -0,0 +1,129 @@
ActiveRecord::Schema[7.1].define(version: 1) do
create_table "solid_queue_blocked_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.string "concurrency_key", null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
end
create_table "solid_queue_claimed_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.bigint "process_id"
t.datetime "created_at", null: false
t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
end
create_table "solid_queue_failed_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.text "error"
t.datetime "created_at", null: false
t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
end
create_table "solid_queue_jobs", force: :cascade do |t|
t.string "queue_name", null: false
t.string "class_name", null: false
t.text "arguments"
t.integer "priority", default: 0, null: false
t.string "active_job_id"
t.datetime "scheduled_at"
t.datetime "finished_at"
t.string "concurrency_key"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
end
create_table "solid_queue_pauses", force: :cascade do |t|
t.string "queue_name", null: false
t.datetime "created_at", null: false
t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
end
create_table "solid_queue_processes", force: :cascade do |t|
t.string "kind", null: false
t.datetime "last_heartbeat_at", null: false
t.bigint "supervisor_id"
t.integer "pid", null: false
t.string "hostname"
t.text "metadata"
t.datetime "created_at", null: false
t.string "name", null: false
t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
end
create_table "solid_queue_ready_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "created_at", null: false
t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
end
create_table "solid_queue_recurring_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "task_key", null: false
t.datetime "run_at", null: false
t.datetime "created_at", null: false
t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
end
create_table "solid_queue_recurring_tasks", force: :cascade do |t|
t.string "key", null: false
t.string "schedule", null: false
t.string "command", limit: 2048
t.string "class_name"
t.text "arguments"
t.string "queue_name"
t.integer "priority", default: 0
t.boolean "static", default: true, null: false
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
end
create_table "solid_queue_scheduled_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "scheduled_at", null: false
t.datetime "created_at", null: false
t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
end
create_table "solid_queue_semaphores", force: :cascade do |t|
t.string "key", null: false
t.integer "value", default: 1, null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
end
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
end

117
db/schema.rb generated Normal file
View File

@@ -0,0 +1,117 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_10_20_031401) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
create_table "admin_users", force: :cascade do |t|
t.string "email", null: false
t.string "password_digest", null: false
t.string "name", null: false
t.datetime "last_login_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_admin_users_on_email", unique: true
end
create_table "api_keys", force: :cascade do |t|
t.string "name", null: false
t.string "key_digest", null: false
t.string "key_prefix", null: false
t.boolean "active", default: true
t.datetime "last_used_at"
t.datetime "expires_at"
t.jsonb "permissions", default: {}
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["active"], name: "index_api_keys_on_active"
t.index ["key_digest"], name: "index_api_keys_on_key_digest", unique: true
t.index ["key_prefix"], name: "index_api_keys_on_key_prefix"
end
create_table "gateways", force: :cascade do |t|
t.string "device_id", null: false
t.string "name"
t.string "api_key_digest", null: false
t.string "status", default: "offline"
t.datetime "last_heartbeat_at"
t.integer "messages_sent_today", default: 0
t.integer "messages_received_today", default: 0
t.integer "total_messages_sent", default: 0
t.integer "total_messages_received", default: 0
t.boolean "active", default: true
t.integer "priority", default: 1
t.jsonb "metadata", default: {}
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["active"], name: "index_gateways_on_active"
t.index ["device_id"], name: "index_gateways_on_device_id", unique: true
t.index ["status"], name: "index_gateways_on_status"
end
create_table "otp_codes", force: :cascade do |t|
t.string "phone_number", null: false
t.string "code", null: false
t.string "purpose", default: "authentication"
t.datetime "expires_at", null: false
t.boolean "verified", default: false
t.datetime "verified_at"
t.integer "attempts", default: 0
t.string "ip_address"
t.jsonb "metadata", default: {}
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["expires_at"], name: "index_otp_codes_on_expires_at"
t.index ["phone_number", "verified", "expires_at"], name: "index_otp_codes_on_phone_number_and_verified_and_expires_at"
t.index ["phone_number"], name: "index_otp_codes_on_phone_number"
end
create_table "sms_messages", force: :cascade do |t|
t.bigint "gateway_id"
t.string "message_id", null: false
t.string "direction", null: false
t.string "phone_number", null: false
t.text "message_body", null: false
t.string "status", default: "pending"
t.text "error_message"
t.integer "retry_count", default: 0
t.datetime "sent_at"
t.datetime "delivered_at"
t.datetime "failed_at"
t.jsonb "metadata", default: {}
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["direction"], name: "index_sms_messages_on_direction"
t.index ["gateway_id"], name: "index_sms_messages_on_gateway_id"
t.index ["message_id"], name: "index_sms_messages_on_message_id", unique: true
t.index ["phone_number", "created_at"], name: "index_sms_messages_on_phone_number_and_created_at"
t.index ["phone_number"], name: "index_sms_messages_on_phone_number"
t.index ["status", "created_at"], name: "index_sms_messages_on_status_and_created_at"
end
create_table "webhook_configs", force: :cascade do |t|
t.string "name", null: false
t.string "url", null: false
t.string "event_type", null: false
t.string "secret_key"
t.boolean "active", default: true
t.integer "timeout", default: 30
t.integer "retry_count", default: 3
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["active"], name: "index_webhook_configs_on_active"
t.index ["event_type"], name: "index_webhook_configs_on_event_type"
end
add_foreign_key "sms_messages", "gateways"
end

143
db/seeds.rb Normal file
View File

@@ -0,0 +1,143 @@
# This file should ensure the existence of records required to run the application in every environment (production,
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
puts "🌱 Seeding database..."
# Create default admin user
puts "\n👤 Creating admin user..."
unless AdminUser.exists?(email: "admin@example.com")
admin = AdminUser.create!(
email: "admin@example.com",
password: "password123",
name: "Admin User"
)
puts "✅ Created Admin User"
puts " Email: admin@example.com"
puts " Password: password123"
puts " ⚠️ IMPORTANT: Change this password in production!"
end
# Create sample API keys for testing
puts "\n📝 Creating API keys..."
api_key_results = []
unless ApiKey.exists?(name: "Development API Key")
result = ApiKey.generate!(
name: "Development API Key",
permissions: {
"send_sms" => true,
"receive_sms" => true,
"manage_otp" => true
}
)
api_key_results << result
puts "✅ Created API Key: #{result[:raw_key]}"
end
unless ApiKey.exists?(name: "Admin API Key")
result = ApiKey.generate!(
name: "Admin API Key",
permissions: {
"send_sms" => true,
"receive_sms" => true,
"manage_otp" => true,
"admin" => true
}
)
api_key_results << result
puts "✅ Created Admin API Key: #{result[:raw_key]}"
end
# Create sample gateway devices
puts "\n📱 Creating sample gateway devices..."
unless Gateway.exists?(device_id: "test-gateway-001")
gateway1 = Gateway.new(
device_id: "test-gateway-001",
name: "Test Gateway 1",
status: "offline",
priority: 1,
active: true
)
raw_key = gateway1.generate_api_key!
puts "✅ Created Gateway: #{gateway1.name}"
puts " Device ID: #{gateway1.device_id}"
puts " API Key: #{raw_key}"
end
unless Gateway.exists?(device_id: "test-gateway-002")
gateway2 = Gateway.new(
device_id: "test-gateway-002",
name: "Test Gateway 2",
status: "offline",
priority: 2,
active: true
)
raw_key = gateway2.generate_api_key!
puts "✅ Created Gateway: #{gateway2.name}"
puts " Device ID: #{gateway2.device_id}"
puts " API Key: #{raw_key}"
end
# Create sample webhook configurations
puts "\n🔗 Creating webhook configurations..."
WebhookConfig.find_or_create_by!(name: "SMS Received Webhook") do |w|
w.url = "https://example.com/webhooks/sms-received"
w.event_type = "sms_received"
w.secret_key = SecureRandom.hex(32)
w.active = false # Disabled by default
w.timeout = 30
w.retry_count = 3
end
puts "✅ Created webhook: SMS Received Webhook"
WebhookConfig.find_or_create_by!(name: "SMS Sent Webhook") do |w|
w.url = "https://example.com/webhooks/sms-sent"
w.event_type = "sms_sent"
w.secret_key = SecureRandom.hex(32)
w.active = false # Disabled by default
w.timeout = 30
w.retry_count = 3
end
puts "✅ Created webhook: SMS Sent Webhook"
WebhookConfig.find_or_create_by!(name: "SMS Failed Webhook") do |w|
w.url = "https://example.com/webhooks/sms-failed"
w.event_type = "sms_failed"
w.secret_key = SecureRandom.hex(32)
w.active = false # Disabled by default
w.timeout = 30
w.retry_count = 3
end
puts "✅ Created webhook: SMS Failed Webhook"
puts "\n✨ Seeding completed!"
puts "\n" + "=" * 80
puts "🔑 API KEYS (Save these - they won't be shown again!)"
puts "=" * 80
api_key_results.each do |result|
puts "\nName: #{result[:api_key].name}"
puts "Key: #{result[:raw_key]}"
puts "Permissions: #{result[:api_key].permissions}"
end
puts "\n" + "=" * 80
puts "📊 Database Summary"
puts "=" * 80
puts "Admins: #{AdminUser.count}"
puts "Gateways: #{Gateway.count}"
puts "API Keys: #{ApiKey.count}"
puts "Webhooks: #{WebhookConfig.count}"
puts "SMS Messages: #{SmsMessage.count}"
puts "OTP Codes: #{OtpCode.count}"
puts "=" * 80
puts "\n🔐 Admin Login"
puts "URL: http://localhost:3000/admin/login"
puts "Email: admin@example.com"
puts "Password: password123"
puts "=" * 80