From 1fdff55544a3fa8a41c0b8704d796ce18b749d45 Mon Sep 17 00:00:00 2001 From: Min Zeya Phyo Date: Tue, 27 Jan 2026 11:24:16 +0630 Subject: [PATCH] SMS gateway api service for android application --- SAAS_INTEGRATION_GUIDE.md | 719 +++++++++++++++++++++++ app/controllers/admin/logs_controller.rb | 5 +- app/views/admin/logs/index.html.erb | 69 ++- 3 files changed, 789 insertions(+), 4 deletions(-) create mode 100644 SAAS_INTEGRATION_GUIDE.md diff --git a/SAAS_INTEGRATION_GUIDE.md b/SAAS_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..6bc6359 --- /dev/null +++ b/SAAS_INTEGRATION_GUIDE.md @@ -0,0 +1,719 @@ +# SaaS Application Integration Guide + +## Overview + +This guide shows how to integrate MySMSAPio into your SaaS application to send SMS and OTP messages. + +## Prerequisites + +1. **MySMSAPio Server**: Running and accessible (e.g., `http://192.168.18.66:3005`) +2. **API Key**: Client API key (starts with `api_live_...`) +3. **Gateway Device**: At least one Android gateway online + +## Quick Start + +### 1. Create an API Key + +**Via Admin Interface**: +1. Go to `http://your-server:3005/admin/api_keys` +2. Click "New API Key" +3. Enter name (e.g., "My SaaS App") +4. Enable permissions: `send_sms`, `receive_sms` +5. Copy the API key (shown only once!) + +**Via Rails Console**: +```ruby +result = ApiKey.generate!( + name: "My SaaS App", + permissions: { + send_sms: true, + receive_sms: true, + send_otp: true, + verify_otp: true + } +) +puts result[:raw_key] +# Save this key: api_live_abc123... +``` + +### 2. Test Connection + +```bash +curl -X GET http://your-server:3005/up \ + -H "Authorization: Bearer api_live_your_key_here" +``` + +Expected: `200 OK` + +## API Endpoints + +### Base URL +``` +http://your-server:3005/api/v1 +``` + +### Authentication +All requests require the `Authorization` header: +``` +Authorization: Bearer api_live_your_key_here +``` + +## Sending SMS + +### Endpoint +``` +POST /api/v1/sms/send +``` + +### Request +```json +{ + "to": "+959123456789", + "message": "Your message here" +} +``` + +### Response (Success - 202 Accepted) +```json +{ + "success": true, + "message_id": "msg_abc123def456...", + "status": "queued" +} +``` + +### Response (Error - 422 Unprocessable Entity) +```json +{ + "error": "Invalid phone number format" +} +``` + +### Example: cURL +```bash +curl -X POST http://your-server:3005/api/v1/sms/send \ + -H "Authorization: Bearer api_live_your_key_here" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "+959123456789", + "message": "Hello from My SaaS App!" + }' +``` + +### Example: JavaScript/Node.js +```javascript +const axios = require('axios'); + +async function sendSMS(phoneNumber, message) { + try { + const response = await axios.post( + 'http://your-server:3005/api/v1/sms/send', + { + to: phoneNumber, + message: message + }, + { + headers: { + 'Authorization': 'Bearer api_live_your_key_here', + 'Content-Type': 'application/json' + } + } + ); + + console.log('SMS sent!'); + console.log('Message ID:', response.data.message_id); + console.log('Status:', response.data.status); + + return response.data.message_id; + } catch (error) { + console.error('Failed to send SMS:', error.response?.data || error.message); + throw error; + } +} + +// Usage +sendSMS('+959123456789', 'Welcome to our service!') + .then(messageId => console.log('Sent:', messageId)) + .catch(err => console.error('Error:', err)); +``` + +### Example: Python +```python +import requests + +def send_sms(phone_number, message): + url = "http://your-server:3005/api/v1/sms/send" + headers = { + "Authorization": "Bearer api_live_your_key_here", + "Content-Type": "application/json" + } + data = { + "to": phone_number, + "message": message + } + + response = requests.post(url, json=data, headers=headers) + + if response.status_code == 202: + result = response.json() + print(f"SMS sent! Message ID: {result['message_id']}") + return result['message_id'] + else: + print(f"Failed: {response.json()}") + raise Exception(response.json().get('error', 'Unknown error')) + +# Usage +message_id = send_sms("+959123456789", "Welcome to our service!") +``` + +### Example: PHP +```php + $phoneNumber, + 'message' => $message + ]; + + $options = [ + 'http' => [ + 'header' => [ + "Authorization: Bearer api_live_your_key_here", + "Content-Type: application/json" + ], + 'method' => 'POST', + 'content' => json_encode($data) + ] + ]; + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + if ($result === FALSE) { + throw new Exception('Failed to send SMS'); + } + + $response = json_decode($result, true); + echo "SMS sent! Message ID: " . $response['message_id'] . "\n"; + + return $response['message_id']; +} + +// Usage +$messageId = sendSMS("+959123456789", "Welcome to our service!"); +?> +``` + +### Example: Ruby +```ruby +require 'httparty' + +def send_sms(phone_number, message) + response = HTTParty.post( + 'http://your-server:3005/api/v1/sms/send', + headers: { + 'Authorization' => 'Bearer api_live_your_key_here', + 'Content-Type' => 'application/json' + }, + body: { + to: phone_number, + message: message + }.to_json + ) + + if response.code == 202 + puts "SMS sent! Message ID: #{response['message_id']}" + response['message_id'] + else + puts "Failed: #{response['error']}" + raise StandardError, response['error'] + end +end + +# Usage +message_id = send_sms("+959123456789", "Welcome to our service!") +``` + +## Sending OTP + +### Endpoint +``` +POST /api/v1/otp/send +``` + +### Request +```json +{ + "phone_number": "+959123456789" +} +``` + +### Response (Success - 200 OK) +```json +{ + "success": true, + "message": "OTP sent successfully", + "expires_in": 300 +} +``` + +### Response (Error - Rate Limited) +```json +{ + "error": "Rate limit exceeded. Maximum 3 OTP per hour for this number" +} +``` + +### Example: JavaScript +```javascript +async function sendOTP(phoneNumber) { + try { + const response = await axios.post( + 'http://your-server:3005/api/v1/otp/send', + { + phone_number: phoneNumber + }, + { + headers: { + 'Authorization': 'Bearer api_live_your_key_here', + 'Content-Type': 'application/json' + } + } + ); + + console.log('OTP sent!'); + console.log('Expires in:', response.data.expires_in, 'seconds'); + + return true; + } catch (error) { + console.error('Failed to send OTP:', error.response?.data || error.message); + throw error; + } +} + +// Usage +sendOTP('+959123456789') + .then(() => console.log('OTP sent successfully')) + .catch(err => console.error('Error:', err)); +``` + +### Example: Python +```python +def send_otp(phone_number): + url = "http://your-server:3005/api/v1/otp/send" + headers = { + "Authorization": "Bearer api_live_your_key_here", + "Content-Type": "application/json" + } + data = { + "phone_number": phone_number + } + + response = requests.post(url, json=data, headers=headers) + + if response.status_code == 200: + result = response.json() + print(f"OTP sent! Expires in {result['expires_in']} seconds") + return True + else: + print(f"Failed: {response.json()}") + raise Exception(response.json().get('error', 'Unknown error')) + +# Usage +send_otp("+959123456789") +``` + +## Verifying OTP + +### Endpoint +``` +POST /api/v1/otp/verify +``` + +### Request +```json +{ + "phone_number": "+959123456789", + "code": "123456" +} +``` + +### Response (Success - 200 OK) +```json +{ + "success": true, + "message": "OTP verified successfully" +} +``` + +### Response (Error - Invalid) +```json +{ + "error": "Invalid or expired OTP" +} +``` + +### Example: JavaScript +```javascript +async function verifyOTP(phoneNumber, code) { + try { + const response = await axios.post( + 'http://your-server:3005/api/v1/otp/verify', + { + phone_number: phoneNumber, + code: code + }, + { + headers: { + 'Authorization': 'Bearer api_live_your_key_here', + 'Content-Type': 'application/json' + } + } + ); + + console.log('OTP verified!'); + return true; + } catch (error) { + console.error('OTP verification failed:', error.response?.data || error.message); + return false; + } +} + +// Usage +const isValid = await verifyOTP('+959123456789', '123456'); +if (isValid) { + console.log('User verified!'); +} else { + console.log('Invalid OTP'); +} +``` + +### Example: Python +```python +def verify_otp(phone_number, code): + url = "http://your-server:3005/api/v1/otp/verify" + headers = { + "Authorization": "Bearer api_live_your_key_here", + "Content-Type": "application/json" + } + data = { + "phone_number": phone_number, + "code": code + } + + response = requests.post(url, json=data, headers=headers) + + if response.status_code == 200: + print("OTP verified successfully!") + return True + else: + print(f"Verification failed: {response.json()}") + return False + +# Usage +if verify_otp("+959123456789", "123456"): + print("User authenticated!") +else: + print("Authentication failed") +``` + +## Checking Message Status + +### Endpoint +``` +GET /api/v1/sms/status/:message_id +``` + +### Response +```json +{ + "message_id": "msg_abc123def456...", + "status": "delivered", + "sent_at": "2025-10-22T10:30:00Z", + "delivered_at": "2025-10-22T10:30:05Z", + "failed_at": null, + "error_message": null +} +``` + +### Status Values +- `queued` - Waiting for gateway +- `pending` - No gateway available, will retry +- `sent` - Sent to gateway device +- `delivered` - Successfully delivered to recipient +- `failed` - Failed to send + +### Example: JavaScript +```javascript +async function checkMessageStatus(messageId) { + try { + const response = await axios.get( + `http://your-server:3005/api/v1/sms/status/${messageId}`, + { + headers: { + 'Authorization': 'Bearer api_live_your_key_here' + } + } + ); + + console.log('Status:', response.data.status); + console.log('Sent at:', response.data.sent_at); + console.log('Delivered at:', response.data.delivered_at); + + return response.data; + } catch (error) { + console.error('Failed to check status:', error.response?.data || error.message); + throw error; + } +} + +// Usage +const status = await checkMessageStatus('msg_abc123def456'); +``` + +## Complete Integration Example + +### User Registration with OTP (JavaScript) + +```javascript +const MySMSAPI = { + baseURL: 'http://your-server:3005/api/v1', + apiKey: 'api_live_your_key_here', + + headers() { + return { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json' + }; + }, + + async sendSMS(phoneNumber, message) { + const response = await axios.post( + `${this.baseURL}/sms/send`, + { to: phoneNumber, message }, + { headers: this.headers() } + ); + return response.data; + }, + + async sendOTP(phoneNumber) { + const response = await axios.post( + `${this.baseURL}/otp/send`, + { phone_number: phoneNumber }, + { headers: this.headers() } + ); + return response.data; + }, + + async verifyOTP(phoneNumber, code) { + try { + const response = await axios.post( + `${this.baseURL}/otp/verify`, + { phone_number: phoneNumber, code }, + { headers: this.headers() } + ); + return response.data.success; + } catch (error) { + return false; + } + }, + + async checkStatus(messageId) { + const response = await axios.get( + `${this.baseURL}/sms/status/${messageId}`, + { headers: this.headers() } + ); + return response.data; + } +}; + +// Usage Example: User Registration Flow +async function registerUser(phoneNumber, userData) { + try { + // Step 1: Send OTP + console.log('Sending OTP...'); + await MySMSAPI.sendOTP(phoneNumber); + console.log('OTP sent! Check your phone.'); + + // Step 2: Wait for user to enter OTP (in real app, this would be a form) + const userEnteredCode = '123456'; // Get from user input + + // Step 3: Verify OTP + console.log('Verifying OTP...'); + const isValid = await MySMSAPI.verifyOTP(phoneNumber, userEnteredCode); + + if (isValid) { + console.log('✅ Phone verified!'); + + // Step 4: Create user account + const user = await createUserInDatabase(phoneNumber, userData); + + // Step 5: Send welcome SMS + const result = await MySMSAPI.sendSMS( + phoneNumber, + `Welcome to our service, ${userData.name}! Your account is now active.` + ); + + console.log('Welcome SMS sent:', result.message_id); + + return { success: true, user }; + } else { + console.log('❌ Invalid OTP'); + return { success: false, error: 'Invalid OTP' }; + } + } catch (error) { + console.error('Registration failed:', error); + return { success: false, error: error.message }; + } +} + +// Example usage +registerUser('+959123456789', { name: 'John Doe', email: 'john@example.com' }); +``` + +## Error Handling + +### Common Errors + +| Status | Error | Cause | Solution | +|--------|-------|-------|----------| +| 401 | Unauthorized | Invalid API key | Check API key is correct | +| 422 | Unprocessable Entity | Invalid phone number | Use international format (+959...) | +| 429 | Too Many Requests | Rate limit exceeded | Wait before retrying | +| 404 | Not Found | Message ID not found | Check message ID is correct | +| 503 | Service Unavailable | No gateway online | Ensure gateway is connected | + +### Rate Limits + +- **SMS Send**: 100 per minute per API key +- **OTP Send**: 3 per hour per phone number +- **OTP Verify**: 3 attempts per OTP code + +## Best Practices + +### 1. Store API Key Securely +```javascript +// ❌ Don't hardcode in frontend +const apiKey = 'api_live_abc123...'; + +// ✅ Use environment variables +const apiKey = process.env.SMS_API_KEY; + +// ✅ Store in backend config +const apiKey = config.get('sms.apiKey'); +``` + +### 2. Handle Errors Gracefully +```javascript +async function sendSMSWithRetry(phoneNumber, message, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await MySMSAPI.sendSMS(phoneNumber, message); + } catch (error) { + if (i === maxRetries - 1) throw error; + await sleep(1000 * (i + 1)); // Exponential backoff + } + } +} +``` + +### 3. Validate Phone Numbers +```javascript +function validatePhoneNumber(phone) { + // Myanmar phone numbers + const myanmarRegex = /^\+959\d{8,9}$/; + return myanmarRegex.test(phone); +} +``` + +### 4. Monitor Message Status +```javascript +async function sendAndTrack(phoneNumber, message) { + const result = await MySMSAPI.sendSMS(phoneNumber, message); + + // Poll status until delivered + const maxAttempts = 30; // 5 minutes (30 * 10 seconds) + for (let i = 0; i < maxAttempts; i++) { + await sleep(10000); // Wait 10 seconds + + const status = await MySMSAPI.checkStatus(result.message_id); + + if (status.status === 'delivered') { + console.log('✅ Message delivered!'); + return status; + } else if (status.status === 'failed') { + console.log('❌ Message failed:', status.error_message); + throw new Error(status.error_message); + } + } + + console.log('⏱️ Delivery timeout'); + return null; +} +``` + +## Testing + +### Test API Key +Use the API Tester in the admin interface: +``` +http://your-server:3005/admin/api_tester +``` + +### Test from Command Line +```bash +# Send SMS +curl -X POST http://your-server:3005/api/v1/sms/send \ + -H "Authorization: Bearer api_live_your_key" \ + -H "Content-Type: application/json" \ + -d '{"to":"+959123456789","message":"Test"}' + +# Send OTP +curl -X POST http://your-server:3005/api/v1/otp/send \ + -H "Authorization: Bearer api_live_your_key" \ + -H "Content-Type: application/json" \ + -d '{"phone_number":"+959123456789"}' + +# Verify OTP +curl -X POST http://your-server:3005/api/v1/otp/verify \ + -H "Authorization: Bearer api_live_your_key" \ + -H "Content-Type: application/json" \ + -d '{"phone_number":"+959123456789","code":"123456"}' +``` + +## Troubleshooting + +### Messages Stuck in "pending" +**Cause**: No gateway online +**Solution**: Ensure at least one Android gateway is connected + +### "Invalid phone number" error +**Cause**: Wrong format +**Solution**: Use international format: `+[country_code][number]` + +### "Rate limit exceeded" +**Cause**: Too many requests +**Solution**: Implement exponential backoff and caching + +### "Unauthorized" error +**Cause**: Invalid or expired API key +**Solution**: Generate new API key in admin interface + +## Support + +- **API Documentation**: See `API_DOCUMENTATION.md` +- **Admin Interface**: `http://your-server:3005/admin` +- **API Tester**: `http://your-server:3005/admin/api_tester` +- **Logs**: `http://your-server:3005/admin/logs` + +## Summary + +✅ **Create API Key** → Get `api_live_...` key +✅ **Send SMS** → `POST /api/v1/sms/send` +✅ **Send OTP** → `POST /api/v1/otp/send` +✅ **Verify OTP** → `POST /api/v1/otp/verify` +✅ **Check Status** → `GET /api/v1/sms/status/:id` + +Your SaaS app is now ready to send SMS and OTP! 🚀 diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb index 9ff6199..6598278 100644 --- a/app/controllers/admin/logs_controller.rb +++ b/app/controllers/admin/logs_controller.rb @@ -1,9 +1,12 @@ module Admin class LogsController < BaseController def index + items_per_page = params[:items]&.to_i || 50 + items_per_page = 50 unless [10, 25, 50, 100].include?(items_per_page) + @pagy, @messages = pagy( apply_filters(SmsMessage).order(created_at: :desc), - items: 50 + items: items_per_page ) respond_to do |format| diff --git a/app/views/admin/logs/index.html.erb b/app/views/admin/logs/index.html.erb index 6ec0550..f405014 100644 --- a/app/views/admin/logs/index.html.erb +++ b/app/views/admin/logs/index.html.erb @@ -128,7 +128,7 @@ <%= msg.phone_number %> -
<%= msg.message_body %>
+
<%= msg.message_body %>
<% if msg.direction == "outbound" %> @@ -209,8 +209,71 @@ <% if @pagy.pages > 1 %>
-
- <%== pagy_nav(@pagy) %> +
+ +
+ Showing + <%= @pagy.from %> + to + <%= @pagy.to %> + of + <%= @pagy.count %> + results +
+ + + + + +
+ <%= form_with url: admin_logs_path, method: :get, local: true, class: "inline-flex items-center gap-2" do |f| %> + + <%= select_tag :items, + options_for_select([10, 25, 50, 100], params[:items] || 50), + onchange: "this.form.submit()", + class: "rounded-md border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500" %> + <% params.except(:items, :authenticity_token).each do |key, value| %> + <%= hidden_field_tag key, value %> + <% end %> + <% end %> +
<% end %>