Boom Konfigurator
Integration Documentation — Connecting the Frontend to NFON and Fortinet FortiPortal APIs
System Architecture
The Boom Konfigurator is a self-service portal for Sunrise Business Hub customers to manage their combined KMU product (Internet, WiFi, Voice/NFON, Security/Fortinet). The architecture follows a three-tier pattern:
Vanilla JS
Middleware Layer
Voice/PBX
Security/UTM
Current State
Complete Frontend SPA
All pages, CRUD operations, i18n (DE/EN), login, navigation
Complete Rails API Layer
Auth, controllers, routes, models, JWT tokens, cookie sessions
Complete Mock Data Layer
PostgreSQL with seed data simulating NFON/Fortinet responses
Pending NFON API Integration
Replace mock Voice controller with real NFON API client
Pending FortiPortal Integration
Replace mock Security controller with real FortiPortal API client
Pending Service Monitoring
Replace mock Internet/WiFi data with real device telemetry
Tech Stack
| Component | Technology | Version |
|---|---|---|
| Backend | Ruby on Rails (API-only) | 8.1 |
| Ruby | Ruby via rbenv | 3.3.6 |
| Database | PostgreSQL | 16 |
| Web Server | Puma behind Nginx | 7.2.0 |
| Frontend | Vanilla JavaScript SPA | ES2020+ |
| Auth | JWT (HS256) in httpOnly cookies | — |
| SSL | Let's Encrypt via Certbot | — |
| Host | AWS EC2 (Ubuntu 22.04) | — |
API Reference
All API endpoints are prefixed with /api/ and return JSON. Authentication is via httpOnly cookies (boom_access and boom_refresh).
Authentication
| Method | Endpoint | Description |
|---|---|---|
POST | /api/auth/login | Login with email + password. Returns user data, sets cookies. |
POST | /api/auth/logout | Clears auth cookies and invalidates session. |
POST | /api/auth/refresh | Refresh access token using refresh cookie. |
GET | /api/auth/me | Returns current user and customer details. |
Login Request
POST /api/auth/login
Content-Type: application/json
{
"email": "demo@example.ch",
"password": "Demo2024!"
}
Login Response
{
"user": {
"id": 2,
"email": "demo@example.ch",
"username": "demo",
"role": "customer"
},
"customer": {
"id": 1,
"company_name": "TechStart GmbH",
"contract_number": "BOOM-2024-001",
"boom_package": "business"
}
}
// + Set-Cookie: boom_access=<JWT>; HttpOnly; Secure; SameSite=Lax; Path=/
// + Set-Cookie: boom_refresh=<JWT>; HttpOnly; Secure; SameSite=Lax; Path=/
Voice Endpoints (NFON)
| Method | Endpoint | Description |
|---|---|---|
GET | /api/voice/extensions | List all extensions |
POST | /api/voice/extensions | Create extension |
PUT | /api/voice/extensions/:id | Update extension |
DELETE | /api/voice/extensions/:id | Delete extension |
GET | /api/voice/call-forwardings | List call forwardings |
POST | /api/voice/call-forwardings | Create/update call forwarding |
PUT | /api/voice/call-forwardings/:id | Update call forwarding |
GET | /api/voice/voicemails | List voicemail configs |
POST | /api/voice/voicemails | Create/update voicemail |
PUT | /api/voice/voicemails/:id | Update voicemail |
GET | /api/voice/ring-groups | List ring groups |
POST | /api/voice/ring-groups | Create ring group |
PUT | /api/voice/ring-groups/:id | Update ring group |
DELETE | /api/voice/ring-groups/:id | Delete ring group |
GET | /api/voice/queues | List call queues |
POST | /api/voice/queues | Create call queue |
PUT | /api/voice/queues/:id | Update call queue |
DELETE | /api/voice/queues/:id | Delete call queue |
Extension JSON Schema
{
"id": 1,
"extension_number": "100",
"display_name": "Empfang",
"phone_number": "+41 44 123 00 00",
"enabled": true,
"status": "online" // online | offline | dnd
}
Call Forwarding JSON Schema
{
"id": 1,
"extension_id": 1,
"busy_forward_to": "+41 44 123 00 00",
"no_answer_forward_to": "voicemail",
"not_registered_forward_to": "+41 79 123 45 67",
"no_answer_ring_time": 20
}
Voicemail JSON Schema
{
"id": 1,
"extension_id": 1,
"enabled": true,
"email_notification": true,
"email_address": "user@example.ch",
"greeting_message": "Please leave a message."
}
Ring Group JSON Schema
{
"id": 1,
"name": "Support",
"description": "Support team",
"member_ids": [3, 4, 5],
"strategy": "sequential" // simultaneous | sequential | circular
}
Call Queue JSON Schema
{
"id": 1,
"name": "Hauptleitung",
"extension": "200",
"agent_ids": [1, 2, 3],
"max_wait_time": 120,
"overflow_destination": "voicemail"
}
Security Endpoints (FortiPortal)
| Method | Endpoint | Description |
|---|---|---|
GET | /api/security/profile | Get security profile (level, features, blocked items) |
PUT | /api/security/profile | Update security level (triggers preset loading) |
GET | /api/security/web-filter | Get web filter categories with status |
PUT | /api/security/web-filter | Toggle web filter / update blocked categories |
GET | /api/security/app-control | Get app control list with status |
PUT | /api/security/app-control | Toggle app control / update blocked apps |
GET | /api/security/firewall-policies | List firewall policies |
POST | /api/security/firewall-policies | Create firewall policy |
PUT | /api/security/firewall-policies/:id | Update firewall policy |
DELETE | /api/security/firewall-policies/:id | Delete firewall policy |
Security Profile JSON Schema
{
"id": 1,
"security_level": "premium", // basic | standard | premium | custom
"web_filter_enabled": true,
"ips_enabled": true,
"app_control_enabled": true,
"blocked_categories": ["malware", "phishing", "adult", "gambling", "proxy", "streaming", "social_media"],
"blocked_apps": ["bittorrent", "tor"],
"allowed_apps": []
}
Firewall Policy JSON Schema
{
"id": 1,
"name": "Allow All Outbound",
"policy_number": 1,
"source_zone": "LAN",
"dest_zone": "WAN",
"service": "ALL",
"action": "accept", // accept | deny
"enabled": true
}
Service Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/services/dashboard | Aggregated service status for dashboard cards |
GET | /api/services/internet | Internet connection details |
GET | /api/services/wifi | WiFi SSIDs and device counts |
NFON API Integration
Overview
NFON provides a cloud PBX system via REST API. The Boom Konfigurator manages extensions, call forwarding, voicemail, ring groups, and queues through this API. The integration requires an NFON admin account with API access for each customer.
https://api.nfon.com/api/v2 or https://portal.nfon.com/api/ depending on your NFON partner tier.
Authentication
NFON uses API key authentication. Each customer account has credentials stored in the customers table:
# Customer model has:
# nfon_account_id — NFON customer/account identifier
#
# Additional credentials to add in .env or Rails credentials:
# NFON_API_BASE_URL=https://api.nfon.com/api/v2
# NFON_API_KEY=your-nfon-api-key
# NFON_API_SECRET=your-nfon-api-secret
API Mapping: Boom ↔ NFON
| Boom Endpoint | NFON API Call | Notes |
|---|---|---|
GET /voice/extensions | GET /customers/{id}/extensions | Map extensionNumber → extension_number |
POST /voice/extensions | POST /customers/{id}/extensions | Create new PBX extension |
PUT /voice/extensions/:id | PUT /extensions/{extId} | Update display name, status, etc. |
DELETE /voice/extensions/:id | DELETE /extensions/{extId} | Remove extension from PBX |
GET /voice/call-forwardings | GET /extensions/{extId}/forwarding | Per-extension forwarding rules |
POST /voice/call-forwardings | PUT /extensions/{extId}/forwarding | NFON uses PUT for upsert |
GET /voice/voicemails | GET /extensions/{extId}/voicemail | Voicemail settings per extension |
POST /voice/voicemails | PUT /extensions/{extId}/voicemail | NFON uses PUT for upsert |
GET /voice/ring-groups | GET /customers/{id}/huntgroups | NFON calls them "hunt groups" |
POST /voice/ring-groups | POST /customers/{id}/huntgroups | Create hunt group |
GET /voice/queues | GET /customers/{id}/queues | ACD queue configuration |
POST /voice/queues | POST /customers/{id}/queues | Create ACD queue |
Service Layer Implementation
Create a new service class at app/services/nfon_service.rb that encapsulates all NFON API communication:
class NfonService
BASE_URL = ENV.fetch('NFON_API_BASE_URL', 'https://api.nfon.com/api/v2')
API_KEY = ENV.fetch('NFON_API_KEY', '')
API_SECRET = ENV.fetch('NFON_API_SECRET', '')
def initialize(customer)
@account_id = customer.nfon_account_id
end
# --- Extensions ---
def list_extensions
resp = get("/customers/#{@account_id}/extensions")
resp.map { |ext| map_extension(ext) }
end
def create_extension(params)
post("/customers/#{@account_id}/extensions", map_to_nfon_ext(params))
end
def update_extension(nfon_ext_id, params)
put("/extensions/#{nfon_ext_id}", map_to_nfon_ext(params))
end
def delete_extension(nfon_ext_id)
delete("/extensions/#{nfon_ext_id}")
end
# --- Call Forwarding ---
def get_forwarding(nfon_ext_id)
resp = get("/extensions/#{nfon_ext_id}/forwarding")
map_forwarding(resp)
end
def update_forwarding(nfon_ext_id, params)
put("/extensions/#{nfon_ext_id}/forwarding", map_to_nfon_fwd(params))
end
# --- Voicemail ---
def get_voicemail(nfon_ext_id)
resp = get("/extensions/#{nfon_ext_id}/voicemail")
map_voicemail(resp)
end
def update_voicemail(nfon_ext_id, params)
put("/extensions/#{nfon_ext_id}/voicemail", map_to_nfon_vm(params))
end
private
def connection
@conn ||= Faraday.new(url: BASE_URL) do |f|
f.request :json
f.response :json
f.headers['Authorization'] = "Bearer #{auth_token}"
f.headers['Content-Type'] = 'application/json'
f.adapter Faraday.default_adapter
end
end
def auth_token
# Implement NFON OAuth token exchange or API key auth
# Cache token in Rails.cache with TTL
Rails.cache.fetch("nfon_token_#{@account_id}", expires_in: 50.minutes) do
resp = Faraday.post("#{BASE_URL}/auth/token", {
api_key: API_KEY, api_secret: API_SECRET
}.to_json, { 'Content-Type' => 'application/json' })
JSON.parse(resp.body)['access_token']
end
end
def get(path)
resp = connection.get(path)
handle_response(resp)
end
def post(path, body)
resp = connection.post(path, body.to_json)
handle_response(resp)
end
def put(path, body)
resp = connection.put(path, body.to_json)
handle_response(resp)
end
def delete(path)
resp = connection.delete(path)
handle_response(resp)
end
def handle_response(resp)
raise NfonError, resp.body unless resp.success?
resp.body
end
# --- Data Mapping ---
def map_extension(nfon_data)
{
extension_number: nfon_data['extensionNumber'],
display_name: nfon_data['displayName'],
phone_number: nfon_data['ddiNumber'],
enabled: nfon_data['active'],
status: map_status(nfon_data['registrationStatus'])
}
end
end
Implementation Guide
Step 1: Add Faraday Gem
# Gemfile
gem 'faraday'
gem 'faraday-retry'
Step 2: Create Service File
# app/services/nfon_service.rb
# (See service layer code above)
Step 3: Update Voice Controller
Replace the mock database calls with the service:
# app/controllers/api/voice_controller.rb
module Api
class VoiceController < BaseController
before_action :require_auth!
def extensions
# BEFORE (mock):
# exts = current_customer.extensions.order(:extension_number)
# render json: exts.map(&:as_api_json)
# AFTER (live):
exts = nfon.list_extensions
render json: exts
end
def create_extension
result = nfon.create_extension(extension_params.to_h)
render json: result, status: :created
end
private
def nfon
@nfon ||= NfonService.new(current_customer)
end
end
end
Step 4: Configure Environment
# .env (add these variables)
NFON_API_BASE_URL=https://api.nfon.com/api/v2
NFON_API_KEY=your_api_key_here
NFON_API_SECRET=your_api_secret_here
Step 5: Add Error Handling
# app/services/nfon_error.rb
class NfonError < StandardError
attr_reader :status, :body
def initialize(response)
@status = response.status
@body = response.body
super("NFON API error #{@status}: #{@body}")
end
end
# In base_controller.rb, add:
rescue_from NfonError do |e|
render json: { error: 'Voice service unavailable', details: e.message },
status: :service_unavailable
end
FortiPortal API Integration
Overview
Fortinet FortiPortal provides a multi-tenant security management API for FortiGate devices. The Boom Konfigurator manages security profiles, web filtering, application control, and firewall policies through this API.
https://your-fortiportal.example.com/fpc/api/v1.
Authentication
FortiPortal uses API token authentication (bearer token) or session-based auth with username/password:
# .env credentials
FORTIPORTAL_API_URL=https://your-fortiportal.example.com/fpc/api/v1
FORTIPORTAL_USERNAME=api-admin
FORTIPORTAL_PASSWORD=your-password
# or
FORTIPORTAL_API_TOKEN=your-api-token
API Mapping: Boom ↔ FortiPortal
| Boom Endpoint | FortiPortal API Call | Notes |
|---|---|---|
GET /security/profile | GET /organizations/{id}/securityprofile | UTM security profile |
PUT /security/profile | PUT /organizations/{id}/securityprofile | Apply preset or custom settings |
GET /security/web-filter | GET /webfilterprofiles/{id} | FortiGuard category filter |
PUT /security/web-filter | PUT /webfilterprofiles/{id} | Toggle categories |
GET /security/app-control | GET /applicationprofiles/{id} | Application signatures |
PUT /security/app-control | PUT /applicationprofiles/{id} | Block/allow applications |
GET /security/firewall-policies | GET /firewallpolicies?org={id} | Policy list for org |
POST /security/firewall-policies | POST /firewallpolicies | Create policy |
PUT /security/firewall-policies/:id | PUT /firewallpolicies/{id} | Update policy |
DELETE /security/firewall-policies/:id | DELETE /firewallpolicies/{id} | Delete policy |
Service Layer Implementation
class FortiPortalService
BASE_URL = ENV.fetch('FORTIPORTAL_API_URL')
def initialize(customer)
@org_id = customer.fortinet_account_id
end
# --- Security Profile ---
def get_profile
resp = get("/organizations/#{@org_id}/securityprofile")
map_profile(resp)
end
def update_profile(level)
case level
when 'basic'
apply_basic_preset
when 'standard'
apply_standard_preset
when 'premium'
apply_premium_preset
end
end
# --- Web Filter ---
def get_web_filter
resp = get("/webfilterprofiles/#{web_filter_profile_id}")
map_web_filter(resp)
end
def update_web_filter(params)
put("/webfilterprofiles/#{web_filter_profile_id}",
map_to_forti_webfilter(params))
end
# --- Firewall Policies ---
def list_policies
resp = get("/firewallpolicies?org=#{@org_id}")
resp.map { |p| map_policy(p) }
end
def create_policy(params)
post("/firewallpolicies", map_to_forti_policy(params))
end
private
def connection
@conn ||= Faraday.new(url: BASE_URL) do |f|
f.request :json
f.response :json
f.headers['Authorization'] = "Bearer #{api_token}"
f.adapter Faraday.default_adapter
end
end
def api_token
if ENV['FORTIPORTAL_API_TOKEN'].present?
ENV['FORTIPORTAL_API_TOKEN']
else
# Session auth: login and cache token
Rails.cache.fetch('fortiportal_token', expires_in: 55.minutes) do
resp = Faraday.post("#{BASE_URL}/auth/signin", {
username: ENV['FORTIPORTAL_USERNAME'],
password: ENV['FORTIPORTAL_PASSWORD']
}.to_json, 'Content-Type' => 'application/json')
JSON.parse(resp.body)['token']
end
end
end
# --- Data Mapping ---
def map_policy(forti_data)
{
id: forti_data['policyid'],
name: forti_data['name'],
policy_number: forti_data['policyid'],
source_zone: forti_data['srcintf'].join(','),
dest_zone: forti_data['dstintf'].join(','),
service: forti_data['service'].map{|s| s['name']}.join(','),
action: forti_data['action'],
enabled: forti_data['status'] == 'enable'
}
end
# Map Boom security levels to FortiPortal presets
def apply_basic_preset
update_web_filter(enabled: true, categories: ['malware', 'phishing'])
update_app_control(enabled: false)
end
end
Implementation Guide
Step 1: Create Service File
# app/services/forti_portal_service.rb
# (See service layer code above)
Step 2: Update Security Controller
# app/controllers/api/security_controller.rb
module Api
class SecurityController < BaseController
before_action :require_auth!
def profile
# BEFORE (mock):
# sp = current_customer.security_profile
# render json: sp.as_api_json
# AFTER (live):
profile = forti.get_profile
render json: profile
end
def firewall_policies
# BEFORE (mock):
# fps = current_customer.firewall_policies.order(:policy_number)
# render json: fps.map(&:as_api_json)
# AFTER (live):
policies = forti.list_policies
render json: policies
end
private
def forti
@forti ||= FortiPortalService.new(current_customer)
end
end
end
Step 3: Configure Environment
# .env (add these variables)
FORTIPORTAL_API_URL=https://your-fortiportal.example.com/fpc/api/v1
FORTIPORTAL_API_TOKEN=your-api-token
# OR session auth:
FORTIPORTAL_USERNAME=api-admin
FORTIPORTAL_PASSWORD=your-password
Step 4: Hybrid Mode (Recommended for Migration)
During migration, you can run in hybrid mode where some data comes from the live API and some from the local database:
# config/application.rb or an initializer
config.x.use_live_nfon = ENV.fetch('USE_LIVE_NFON', 'false') == 'true'
config.x.use_live_fortinet = ENV.fetch('USE_LIVE_FORTINET', 'false') == 'true'
# In controller:
def extensions
if Rails.configuration.x.use_live_nfon
render json: nfon.list_extensions
else
render json: current_customer.extensions.order(:extension_number).map(&:as_api_json)
end
end
Environment Configuration
Required Environment Variables
| Variable | Description | Required |
|---|---|---|
SECRET_KEY_BASE | Rails secret for JWT signing | Yes |
DATABASE_URL | PostgreSQL connection string | Yes |
RAILS_ENV | Environment (production) | Yes |
NFON_API_BASE_URL | NFON API base URL | For live |
NFON_API_KEY | NFON API key | For live |
NFON_API_SECRET | NFON API secret | For live |
FORTIPORTAL_API_URL | FortiPortal API URL | For live |
FORTIPORTAL_API_TOKEN | FortiPortal API token | For live |
USE_LIVE_NFON | Enable live NFON (true/false) | Optional |
USE_LIVE_FORTINET | Enable live FortiPortal (true/false) | Optional |
Database Schema
The local database stores customer data, user accounts, and cached/mock data. When switching to live APIs, the voice and security tables can serve as a local cache for faster reads.
Tables
| Table | Purpose | Live Mode |
|---|---|---|
users | Authentication accounts | Always local |
customers | Customer profiles + API account IDs | Always local |
sessions | JWT refresh token tracking | Always local |
extensions | PBX extensions | Cache or NFON live |
call_forwardings | Call forwarding rules | Cache or NFON live |
voicemails | Voicemail configuration | Cache or NFON live |
ring_groups | Hunt groups / ring groups | Cache or NFON live |
call_queues | ACD queues | Cache or NFON live |
security_profiles | UTM security settings | Cache or FortiPortal live |
firewall_policies | Firewall policy rules | Cache or FortiPortal live |
Key Relationships
Customer (1) ──> (*) Extension ──> (1) CallForwarding
──> (1) Voicemail
Customer (1) ──> (*) RingGroup
Customer (1) ──> (*) CallQueue
Customer (1) ──> (1) SecurityProfile
Customer (1) ──> (*) FirewallPolicy
Customer (1) ──> (1) User
Deployment Steps
Going Live with NFON
- Obtain NFON Partner API credentials from your NFON account manager
- Add
faradayandfaraday-retrygems, runbundle install - Create
app/services/nfon_service.rbwith the service layer code - Create
app/services/nfon_error.rbfor error handling - Update
.envwith NFON API credentials - Set
USE_LIVE_NFON=truein environment - Test with a staging NFON account first
- Map
nfon_account_idin the customers table for each customer - Restart Puma to load new code
Going Live with FortiPortal
- Obtain FortiPortal API access from your Fortinet partner portal
- Create
app/services/forti_portal_service.rb - Update
.envwith FortiPortal credentials - Set
USE_LIVE_FORTINET=truein environment - Map
fortinet_account_idin the customers table - Test security level presets map correctly to FortiPortal profiles
- Verify firewall policy CRUD against live FortiGate device
- Restart Puma
USE_LIVE_NFON / USE_LIVE_FORTINET flags) to gradually migrate one service at a time. This allows you to test NFON integration while keeping Fortinet on mock data, or vice versa.
Server Details
# SSH access
ssh ubuntu@16.171.52.195
# Application root
/home/ubuntu/sbhboom/
# Start Puma
cd /home/ubuntu/sbhboom
export PATH=/home/ubuntu/.rbenv/bin:/home/ubuntu/.rbenv/shims:$PATH
eval "$(cat .env | sed 's/^/export /')"
rm -f tmp/sockets/puma.sock
nohup bundle exec puma -b unix:///home/ubuntu/sbhboom/tmp/sockets/puma.sock \
-e production > /tmp/puma_sbhboom.log 2>&1 & disown
# Domain
https://sbhboom.accountplan.ch
# Demo credentials
Email: demo@example.ch
Password: Demo2024!
Boom Konfigurator — Sunrise Business Hub © 2024–2026