diff --git a/.env.example b/.env.example
index 6d30773..cae1c6c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,74 +1,100 @@
+# Environment
+ENV="dev"
+
# RabbitMQ
-RABBITMQ_DEFAULT_USER="__RABBITMQ_DEFAULT_USER__"
-RABBITMQ_DEFAULT_PASS="__RABBITMQ_DEFAULT_PASS__"
+RABBITMQ_DEFAULT_USER=
+RABBITMQ_DEFAULT_PASS=
# Authentication JWT
-JWT_PRIVATE_KEY="__JWT_PRIVATE_KEY__"
-JWT_PUBLIC_KEY="__JWT_PUBLIC_KEY__"
+JWT_PRIVATE_KEY=
+JWT_PUBLIC_KEY=
# Google OAuth
-GOOGLE_CALLBACK_URL="__GOOGLE_CALLBACK_URL__"
-GOOGLE_CLIENT_ID="__GOOGLE_CLIENT_ID__"
-GOOGLE_CLIENT_SECRET="__GOOGLE_CLIENT_SECRET__"
+GOOGLE_CALLBACK_URL=
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
# Apple OAuth
-APPLE_CLIENT_ID="__APPLE_CLIENT_ID__"
-APPLE_CLIENT_SECRET="__APPLE_CLIENT_SECRET__"
-APPLE_CLIENT_KEY="__APPLE_CLIENT_KEY__"
-APPLE_CERTIFICATE_KEY="__APPLE_CERTIFICATE_KEY__"
+APPLE_CLIENT_ID=
+APPLE_CLIENT_SECRET=
+APPLE_CLIENT_KEY=
+APPLE_CERTIFICATE_KEY=
# Auth Service
-AUTH_POSTGRES_PASSWORD="__AUTH_POSTGRES_PASSWORD__"
-AUTH_SECRET_KEY="__AUTH_SECRET_KEY__"
-DEFAULT_TENANT_HOST="__DEFAULT_TENANT_HOST__"
-ROOT_API_KEY="__ROOT_API_KEY__"
+AUTH_POSTGRES_PASSWORD=
+AUTH_SECRET_KEY=
+DEFAULT_TENANT_HOST="localhost"
+ROOT_API_KEY=
# S3 Service
-AWS_ACCESS_KEY_ID="__AWS_ACCESS_KEY_ID__"
-AWS_SECRET_ACCESS_KEY="__AWS_SECRET_ACCESS_KEY__"
-AWS_STORAGE_BUCKET_NAME="spacedf-s3-1f841081-c8e98ef7bb21"
-AWS_REGION="__AWS_REGION__"
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_STORAGE_BUCKET_NAME=
+AWS_REGION="us-east-1"
# Redis Service
-REDIS_HOST="__REDIS_HOST__"
+REDIS_HOST=
# Dashboard Service
-DASHBOARD_POSTGRES_PASSWORD="__DASHBOARD_POSTGRES_PASSWORD__"
-DASHBOARD_SECRET_KEY="__DASHBOARD_SECRET_KEY__"
+DASHBOARD_POSTGRES_PASSWORD=
+DASHBOARD_SECRET_KEY=
# Device Service
-DEVICE_POSTGRES_PASSWORD="__DEVICE_POSTGRES_PASSWORD__"
-DEVICE_SECRET_KEY="__DEVICE_SECRET_KEY__"
-TELEMETRY_SERVICE_URL="__TELEMETRY_SERVICE_URL__"
+DEVICE_POSTGRES_PASSWORD=
+DEVICE_SECRET_KEY=
+TELEMETRY_SERVICE_URL="http://telemetry:8080"
# EMQX Service
-EMQX_USERNAME="__EMQX_USERNAME__"
-EMQX_PASSWORD="__EMQX_PASSWORD__"
+EMQX_HOST="http://emqx:18083/api/v5"
+EMQX_USERNAME=
+EMQX_PASSWORD=
# Broker Bridge Service
-MQTT_BROKER_BRIDGE_USERNAME="__MQTT_BROKER_BRIDGE_USERNAME__"
-MQTT_BROKER_BRIDGE_PASSWORD="__MQTT_BROKER_BRIDGE_PASSWORD__"
-MQTT_TOPICS="__MQTT_TOPICS__"
+MQTT_BROKER_BRIDGE_USERNAME="BrokerBridgeService"
+MQTT_BROKER_BRIDGE_PASSWORD=
+MQTT_TOPICS="tenant/+/transformed/device/location"
# Acount AWS to access to SES service
-EMAIL_BACKEND="__EMAIL_BACKEND__"
-EMQX_HOST="__EMQX_HOST__"
-EMAIL_PORT="__EMAIL_PORT__"
-EMAIL_USE_TLS="__EMAIL_USE_TLS__"
-EMAIL_HOST_USER="__EMAIL_HOST_USER__"
-EMAIL_HOST_PASSWORD="__EMAIL_HOST_PASSWORD__"
-DEFAULT_FROM_EMAIL="__DEFAULT_FROM_EMAIL__"
+EMAIL_HOST_USER=
+EMAIL_HOST_PASSWORD=
+DEFAULT_FROM_EMAIL="no-reply@gmail.com"
+EMAIL_BACKEND=
+EMAIL_PORT=
+EMAIL_USE_TLS=
# MPA service
-MQTT_BROKER="__MQTT_BROKER__"
-MQTT_USERNAME="__MQTT_USERNAME__"
-MQTT_PASSWORD="__MQTT_USERNAME__"
-MQTT_PORT="__MQTT_PORT__"
-MQTT_CLIENT_ID="__MQTT_CLIENT_ID__"
-MQTT_TOPIC="__MQTT_TOPIC__"
+MQTT_BROKER="emqx"
+MQTT_USERNAME="MPAService"
+MQTT_PASSWORD=
+MQTT_PORT="1883"
+MQTT_CLIENT_ID="mpa-service-mqtt-bridge"
+MQTT_TOPIC="tenant/{tenant}/device/data"
+
+# Bootstrap service
+HOST="http://localhost:8000"
+BOOTSTRAP_POSTGRES_PASSWORD=
+CORS_ALLOWED_ORIGINS="http://localhost,http://localhost:3000,http://localhost:3001"
+BOOTSTRAP_SECRET_KEY=
# Organization initialization
-ORG_NAME="__ORG_NAME__"
-ORG_SLUG="__ORG_SLUG__"
-OWNER_EMAIL="__OWNER_EMAIL__"
-OWNER_PASSWORD="__OWNER_PASSWORD__"
\ No newline at end of file
+ORG_NAME=
+ORG_SLUG=
+OWNER_EMAIL=
+OWNER_PASSWORD=
+
+# NextAuth configuration
+PORTAL_NEXTAUTH_SECRET=
+HOST_FRONTEND_ADMIN="http://localhost:3001"
+PORTAL_NEXTAUTH_URL="http://localhost:3001"
+PORTAL_AUTH_API="http://haproxy:3000"
+
+#This is the random key for nextauth - generate here: https://generate-secret.vercel.app/32
+DASHBOARD_NEXTAUTH_SECRET=
+MAPTILER_API_KEY="H3MD3Z1wmzMsKpuVstcr"
+DASHBOARD_MQTT_USERNAME=
+DASHBOARD_MQTT_PASSWORD=
+DASHBOARD_MQTT_PROTOCOL="ws"
+DASHBOARD_MQTT_PORT="8883"
+DASHBOARD_MQTT_BROKER="emqx.localhost:8000"
+DASHBOARD_NEXTAUTH_URL="http://localhost:3000"
+ASHBOARD_AUTH_API="http://haproxy:3000"
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 1659562..13060c2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -18,10 +18,6 @@
path = django-common-utils
url = git@github.com:Space-DF/django-common-utils.git
branch = dev
-[submodule "haproxy"]
- path = haproxy
- url = git@github.com:Space-DF/haproxy.git
- branch = dev
[submodule "transformer-service"]
path = transformer-service
url = git@github.com:Space-DF/transformer-service.git
diff --git a/CLA.md b/CLA.md
new file mode 100644
index 0000000..093c6b7
--- /dev/null
+++ b/CLA.md
@@ -0,0 +1,67 @@
+# Contributor License Agreement (CLA)
+
+**Version 1.0**
+
+This Contributor License Agreement (“**Agreement**”) is entered into by **you** (“**Contributor**”) and **Digital Fortress** (“**Company**”) regarding your contributions to the **SpaceDF** project (“**Project**”).
+
+By submitting any Contribution to the Project, you agree to the following terms:
+
+## 1. Definitions
+
+- **“Contribution”** means any source code, documentation, design, or other material submitted by you to the Project.
+- **“Submit”** means any form of electronic, written, or verbal communication intended to be included in the Project, including but not limited to pull requests, patches, issues, or comments.
+
+## 2. Copyright Ownership
+
+- You retain ownership of the copyright in your Contributions.
+- Nothing in this Agreement transfers ownership of your intellectual property to the Company.
+
+## 3. License Grant
+
+You grant **Digital Fortress** a **perpetual, worldwide, non-exclusive, royalty-free, and irrevocable license** to:
+
+- Use
+- Modify
+- Distribute
+- Re-license
+- Sublicense
+- Commercialize
+
+your Contributions as part of the Project or in any related products or services.
+
+This includes, but is not limited to, use in **proprietary**, **SaaS**, and **enterprise** offerings.
+
+## 4. Patent Grant
+
+You grant Digital Fortress a **perpetual, worldwide, royalty-free license** to any patent claims you own that are necessarily infringed by your Contributions.
+
+## 5. Representations
+
+You represent and warrant that:
+
+- You have the legal right to submit the Contributions.
+- The Contributions do not violate or infringe upon any third-party rights.
+- If your employer or organization has intellectual property policies, you have obtained all necessary permissions to make the Contributions.
+
+## 6. No Obligation
+
+The Company is **not obligated** to:
+
+- Accept your Contributions.
+- Provide any form of compensation.
+- Include your Contributions in any release or distribution.
+
+## 7. Public Attribution
+
+The Company **may**, but is not required to, publicly acknowledge or attribute your Contributions.
+
+## 8. License Compatibility
+
+- Your Contributions will be licensed to users under the Project’s open-source license (e.g., **Apache License 2.0**).
+- This Agreement governs only the relationship between you and the Company and does not modify the Project’s open-source license.
+
+## 9. Governing Law
+
+This Agreement shall be governed by and construed in accordance with the laws of **Vietnam**.
+
+By submitting a Contribution, you confirm that you have read, understood, and agree to the terms of this Agreement.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..10398cd
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2026 Digital Fortress
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
index 83eaea1..d2092fd 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,345 @@
-
-
-
-
-
-
-
-
-
----
-
-# Django template
+# SpaceDF Core
## Prerequisites
- [Docker](https://www.docker.com/)
-- Docker compose
+- [Docker Compose](https://docs.docker.com/compose/install/)
-## Clone source code
+## Quick Start
-```
+### 1. Clone the Repository
+
+```bash
git clone -b dev --recurse-submodules git@github.com:Space-DF/spacedf-backend.git
+cd spacedf-backend
+git submodule update --init --recursive
```
-To update submodules in exist directory
+### 2. Create Environment Configuration
+
+Create `.env` file from `.env.example`:
+```bash
+cp .env.example .env
```
-git submodule update --init --recursive
+
+**Auto-generate environment variables:**
+
+Run the setup script to automatically generate all required keys and secrets:
+
+```bash
+chmod +x generate-keys.sh
+./generate-keys.sh
```
-## Setup
+This script will automatically:
+- Generate RSA 2048-bit JWT private and public keys
+- Generate random secrets for authentication and NextAuth
+- Populate all essential environment variables including:
+ - Database passwords
+ - Message broker credentials (RabbitMQ, EMQX, MQTT)
+ - Service secret keys
+ - Organization configuration
+ - OAuth and API secrets
+
+The generated values are written directly to `.env`, replacing any existing placeholder values.
-### Setup environment
+**Optional: Manual Configuration**
-1. Setup [Docker](https://www.docker.com/)
-```commandline
-brew install colima
-colima start
+If you need to customize any values after running the script, edit `.env` directly. Variables that should be customized:
+
+```bash
+# Organization (customize as needed)
+ORG_NAME="Your Organization"
+ORG_SLUG="your-org"
+OWNER_EMAIL="your-email@example.com"
+OWNER_PASSWORD=""
+# ... other OAuth configuration
```
-2. Setup docker-compose
-```commandline
-brew install docker-compose
+
+For detailed information on all environment variables, see the **Environment Configuration (.env)** section below.
+
+### 3. Start Services
+
+```bash
+chmod +x entrypoint.sh
+./entrypoint.sh
```
-### Launch
-```commandline
-docker-compose up
+### 4. Access Services
+
+Once ready, access:
+- **Frontend Admin**: http://localhost:3001
+- **Frontend Dashboard**: http://your-org.localhost:3000
+- **API Docs**: http://localhost:8000/docs
+- **EMQX Dashboard**: http://localhost:18083
+- **RabbitMQ**: http://localhost:15672
+
+## Common Commands
+
+```bash
+# View logs
+docker-compose -f docker-compose.yml -p spacedf-core logs -f
+
+# Stop services
+docker-compose -f docker-compose.yml -p spacedf-core stop
+
+# Restart specific service
+docker-compose -f docker-compose.yml -p spacedf-core restart auth
+
+# Fresh start (removes data)
+docker-compose -f docker-compose.yml -p spacedf-core down -v
+./entrypoint.sh
```
-### API documentation
-http://localhost/docs
+## Troubleshooting
+
+**Services not starting**: Check logs with `docker-compose logs -f` and verify `.env` configuration
+
+**Port already in use**: Stop conflicting services or change port mappings in `docker-compose.yml`
+
+**Database issues**: Restart databases: `docker-compose restart auth_postgres bootstrap_postgres`
+
+**Services keep crashing**: Rebuild with `docker-compose down -v && docker-compose build --no-cache && ./entrypoint.sh`
+
+#### Core Environment Variables
+
+**ENV** - Deployment environment mode (dev, local, prod). Determines which HAProxy configuration and feature sets are active.
+- Default: `local`
+- Example: `dev` or `prod`
+
+**HOST** - External hostname for JWT issuer validation. This is the public-facing URL used for JWT token verification.
+- Default: `http://localhost:8000`
+- Example: `http://api.example.com`
+
+**HOST_FRONTEND_ADMIN** - Admin frontend URL for reference and configuration.
+- Default: `http://localhost:3001`
+- Example: `http://admin.example.com`
+
+#### Organization Configuration
+
+**ORG_NAME** - Display name of your organization, used in UI and initialization.
+- Default: `Default Organization`
+- Example: `My Company Inc.`
+
+**ORG_SLUG** - URL-safe identifier for your organization. Used in hostnames, aliases, and internal references. Should be lowercase with hyphens only.
+- Default: `default-org`
+- Example: `my-company`
+
+**OWNER_EMAIL** - Email address of the initial organization owner. This account is created during bootstrap.
+- Example: `admin@example.com`
+
+**OWNER_PASSWORD** - Password for the initial owner account. Should be strong and changed after first login.
+- Example: `changeme123#Test`
+
+#### HAProxy Configuration
+
+**ORG_SLUG** - Used to create dynamic network aliases like `${ORG_SLUG}.haproxy` for internal service communication.
+
+#### Database Configuration
+
+**AUTH_POSTGRES_PASSWORD** - Password for Auth Service PostgreSQL database.
+- Default: `postgres`
+- Database name: `auth_service`
+- Port: `5434`
+
+**BOOTSTRAP_POSTGRES_PASSWORD** - Password for Bootstrap Service PostgreSQL database.
+- Default: `postgres`
+- Database name: `bootstrap_service`
+- Port: `5433`
+
+**DASHBOARD_POSTGRES_PASSWORD** - Password for Dashboard Service PostgreSQL database.
+- Default: `postgres`
+- Database name: `dashboard_service`
+- Port: `5435`
+
+**DEVICE_POSTGRES_PASSWORD** - Password for Device Service PostgreSQL database.
+- Default: `postgres`
+- Database name: `device_service`
+- Port: `5436`
+
+**TIMESCALEDB_PASSWORD** - Password for TimescaleDB (used by Telemetry Service).
+- Default: `postgres`
+- Database name: `spacedf_telemetry`
+- Port: `5437`
+
+#### Service Secret Keys
+
+Each service requires a unique secret key for encryption and data protection:
+
+**AUTH_SECRET_KEY** - Secret key used by Auth Service for session encryption and token generation.
+
+**BOOTSTRAP_SECRET_KEY** - Secret key used by Bootstrap Service for setup operations.
+
+**DASHBOARD_SECRET_KEY** - Secret key used by Dashboard Service for authentication.
+
+**DEVICE_SECRET_KEY** - Secret key used by Device Service for secure operations.
+
+#### JWT Authentication
+
+**JWT_PRIVATE_KEY** - RSA private key used to sign JWT tokens. Keep this secure and never expose it.
+- Format: PEM-encoded RSA private key
+- Used by: Auth Service for token generation
+
+**JWT_PUBLIC_KEY** - RSA public key used to verify JWT tokens. Can be safely shared.
+- Format: PEM-encoded RSA public key
+- Used by: HAProxy and all services for token verification
+
+#### Message Broker Configuration
+
+**RABBITMQ_DEFAULT_USER** - Username for RabbitMQ message broker connection.
+- Default: `default`
+
+**RABBITMQ_DEFAULT_PASS** - Password for RabbitMQ message broker connection.
+- Default: `password`
+
+**REDIS_HOST** - Redis connection string for caching and session storage.
+- Default: `redis://redis:6379/1`
+- Format: `redis://[user:password@]host:port/database`
+
+#### MQTT/EMQX Broker Configuration
-### Default Local Database
+**EMQX_HOST** - EMQX API endpoint for management operations.
+- Default: `http://emqx:18083/api/v5`
+
+**EMQX_USERNAME** - Username for EMQX dashboard and API access.
+- Default: `user1`
+
+**EMQX_PASSWORD** - Password for EMQX dashboard and API access.
+- Default: `password123`
+
+#### MPA Service MQTT Configuration
+
+**MQTT_BROKER** - Hostname of the MQTT broker (EMQX).
+- Default: `emqx`
+
+**MQTT_USERNAME** - MQTT username for MPA Service connection.
+- Default: `MPAService`
+
+**MQTT_PASSWORD** - MQTT password for MPA Service connection.
+- Default: `DF@1234`
+
+**MQTT_PORT** - MQTT broker port.
+- Default: `1883`
+
+**MQTT_CLIENT_ID** - Client identifier for MQTT connections from MPA Service.
+- Default: `mpa-service-mqtt-bridge`
+
+**MQTT_TOPIC** - Topic pattern for MPA Service to subscribe/publish. Use `{tenant}` placeholder for dynamic tenant names.
+- Default: `tenant/{tenant}/device/data`
+
+#### Broker Bridge Configuration
+
+**MQTT_BROKER_BRIDGE_USERNAME** - MQTT username for Broker Bridge Service.
+- Default: `BrokerBridgeService`
+
+**MQTT_BROKER_BRIDGE_PASSWORD** - MQTT password for Broker Bridge Service.
+- Default: `DF@1234`
+
+**MQTT_TOPICS** - Comma-separated list of MQTT topics for Broker Bridge to subscribe to. Supports wildcards.
+- Default: `tenant/+/transformed/device/location`
+
+#### AWS S3 Configuration
+
+**AWS_ACCESS_KEY_ID** - AWS IAM access key for S3 operations.
+
+**AWS_SECRET_ACCESS_KEY** - AWS IAM secret access key for S3 operations.
+
+**AWS_STORAGE_BUCKET_NAME** - Name of the S3 bucket for file storage.
+- Example: `spacedf-storage-bucket`
+
+**AWS_REGION** - AWS region where S3 bucket is located.
+- Default: `ap-southeast-1`
+
+#### Email Service Configuration
+
+**EMAIL_HOST_USER** - AWS SES SMTP username or email service credentials.
+
+**EMAIL_HOST_PASSWORD** - AWS SES SMTP password or email service credentials.
+
+**DEFAULT_FROM_EMAIL** - Default sender email address for all outgoing emails.
+- Example: `no-reply@example.com`
+
+#### OAuth Authentication (Google)
+
+**GOOGLE_CALLBACK_URL** - Google OAuth callback URL (redirect after login).
+- Example: `https://api.example.com/api/console/google/callback/`
+
+**GOOGLE_CLIENT_ID** - Google OAuth application client ID from Google Cloud Console.
+
+**GOOGLE_CLIENT_SECRET** - Google OAuth application client secret.
+
+#### OAuth Authentication (Apple)
+
+**APPLE_CLIENT_ID** - Apple OAuth application identifier.
+- Example: `com.example.app`
+
+**APPLE_CLIENT_SECRET** - Apple OAuth application secret.
+
+**APPLE_CLIENT_KEY** - Apple OAuth client key.
+
+**APPLE_CERTIFICATE_KEY** - Apple OAuth certificate private key (PEM format).
+
+#### NextAuth Configuration
+
+**PORTAL_NEXTAUTH_URL** - Portal NextAuth URL for authentication callbacks.
+- Default: `http://localhost:3001`
+
+**PORTAL_NEXTAUTH_SECRET** - Secret key for NextAuth portal encryption. Generate using: https://generate-secret.vercel.app/32
+- Must be a random 32-character string
+
+**DASHBOARD_NEXTAUTH_SECRET** - Secret key for dashboard NextAuth encryption.
+- Must be a random string
+
+**PORTAL_AUTH_API** - Auth API endpoint used by portal for authentication.
+- Default: `http://haproxy:3000`
+- This points to HAProxy which routes to Auth Service
+
+#### API Keys and Tokens
+
+**ROOT_API_KEY** - Master API key for privileged operations and admin access. Keep this secure.
+
+#### Application Settings
+
+**DEFAULT_TENANT_HOST** - Default tenant hostname for multi-tenant setup.
+- Default: `localhost`
+
+**TELEMETRY_SERVICE_URL** - Endpoint for telemetry service API calls.
+- Default: `http://telemetry:8080`
+
+**CORS_ALLOWED_ORIGINS** - Comma-separated list of allowed origins for CORS. Controls which frontend URLs can access the API.
+- Default: `http://localhost,http://localhost:3000,http://localhost:3001`
+- Example: `http://localhost:3000,https://app.example.com`
+
+## Services Overview
+
+### Python Django Services
+- **Auth Service** - Authentication and OAuth credentials management
+- **Bootstrap Service** - Initial setup and configuration service
+- **Dashboard Service** - Dashboard and UI backend
+- **Device Service** - Device management service
+- **Django Common Utils** - Shared utilities library
+
+### Go Services
+- **Broker Bridge Service** - Bridge between message brokers
+- **MPA Service** - Multi-Protocol Adapter service
+- **Telemetry Service** - Telemetry data collection and processing
+- **Transformer Service** - Data transformation service
+
+### Infrastructure
+- **EMQX** - MQTT message broker
+- **HAProxy** - Load balancer and reverse proxy
+
+### Default Local Database Configuration
#### Auth service
- Host: `localhost:5434`
- Username: `postgres`
- Password: `postgres`
- Database: `auth_service`
+#### Bootstrap service
+- Host: `localhost:5433`
+- Username: `postgres`
+- Password: `postgres`
+- Database: `bootstrap_service`
#### Dashboard service
- Host: `localhost:5435`
- Username: `postgres`
@@ -72,26 +357,7 @@ http://localhost/docs
- Database: `spacedf_telemetry`
## License
+Licensed under the Apache License, Version 2.0
+See the LICENSE file for details.
-This project is Copyright (c) 2023 and onwards Digital Fortress. It is free software and may be redistributed under the terms specified in the [LICENSE] file.
-
-[LICENSE]: /LICENSE
-
-## About
-
-
-
-
-
-
-
-This project is made and maintained by Digital Fortress.
-
-We are an experienced team in R&D, software, hardware, cross-platform mobile and DevOps.
-
-See more of [our projects][projects] or do you need to complete one?
-
--> [Let’s connect with us][website]
-
-[projects]: https://github.com/digitalfortress-dev
-[website]: https://www.digitalfortress.dev
+[](https://df.technology/)
diff --git a/auth-service b/auth-service
index 4f2da05..8be5ef3 160000
--- a/auth-service
+++ b/auth-service
@@ -1 +1 @@
-Subproject commit 4f2da059c7530dc775ecf63310fc70ff8c815448
+Subproject commit 8be5ef3c4dede130e4e7553848ed27c1bd468804
diff --git a/bootstrap-service b/bootstrap-service
index f3c92d6..5d1e554 160000
--- a/bootstrap-service
+++ b/bootstrap-service
@@ -1 +1 @@
-Subproject commit f3c92d60b5253edeabe08eb8d149c2343d48abea
+Subproject commit 5d1e5549da2e7f63211b8563c7dc496aea4e332c
diff --git a/broker-bridge-service b/broker-bridge-service
index 5f6d962..9773652 160000
--- a/broker-bridge-service
+++ b/broker-bridge-service
@@ -1 +1 @@
-Subproject commit 5f6d9623a17d213fb6f882cd5a7a4b2c51358aee
+Subproject commit 9773652782e0a0abbfc20c963a76f25de2277813
diff --git a/dashboard-service b/dashboard-service
index c4adc84..22e8803 160000
--- a/dashboard-service
+++ b/dashboard-service
@@ -1 +1 @@
-Subproject commit c4adc84611c7c967737a9a6c98615c48d7fa4afa
+Subproject commit 22e880346103a9936cd675332a5ef10734dcaeba
diff --git a/django-common-utils b/django-common-utils
index fc25176..f05d9ec 160000
--- a/django-common-utils
+++ b/django-common-utils
@@ -1 +1 @@
-Subproject commit fc25176ab93ccc94e7a12dd3499c28fdbc72b9fb
+Subproject commit f05d9ec7d02204ba78dcdbfb968571702440928b
diff --git a/docker-compose.yml b/docker-compose.yml
index b529f09..04ab234 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,17 +12,18 @@ services:
- "15672:15672"
command: >
bash -c "
- rabbitmq-plugins enable rabbitmq_mqtt &&
+ rabbitmq-plugins enable --offline rabbitmq_mqtt &&
rabbitmq-server
"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
+ restart: unless-stopped
healthcheck:
- test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
+ test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
interval: 10s
- timeout: 30s
+ timeout: 60s
retries: 5
- start_period: 60s
+ start_period: 120s
# EMQX
emqx:
@@ -81,27 +82,61 @@ services:
- emqx-etc:/opt/emqx/etc
depends_on:
rabbitmq:
- condition: service_started
+ condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "emqx", "ctl", "status"]
interval: 10s
timeout: 5s
retries: 5
+
+ bootstrap_postgres:
+ container_name: bootstrap_postgres
+ hostname: bootstrap_postgres
+ image: postgres:15
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=${BOOTSTRAP_POSTGRES_PASSWORD}
+ - POSTGRES_DB=bootstrap_service
+ ports:
+ - "5433:5432"
+ volumes:
+ - bootstrap-postgres-vlm:/var/lib/postgresql/data
+ healthcheck:
+ test: "pg_isready --username=$$POSTGRES_USER && psql --username=$$POSTGRES_USER --list"
+ timeout: 10s
+ retries: 20
+
# bootstrap service
bootstrap:
container_name: bootstrap_service
- build:
- context: .
- dockerfile: ./bootstrap-service/Dockerfile
+ image: ghcr.io/space-df/bootstrap-service:latest
+ pull_policy: always
environment:
- ENV: dev
+ ENV: ${ENV}
EMQX_USERNAME: ${EMQX_USERNAME}
EMQX_PASSWORD: ${EMQX_PASSWORD}
ORG_NAME: ${ORG_NAME}
ORG_SLUG: ${ORG_SLUG}
OWNER_EMAIL: ${OWNER_EMAIL}
OWNER_PASSWORD: ${OWNER_PASSWORD}
+ CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
+ REDIS_HOST: ${REDIS_HOST}
+ HOST: ${HOST}
+ DB_NAME: bootstrap_service
+ DB_USERNAME: postgres
+ DB_PASSWORD: ${BOOTSTRAP_POSTGRES_PASSWORD}
+ DB_HOST: bootstrap_postgres
+ DB_PORT: 5432
+ JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
+ JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
+ AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
+ AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
+ AWS_STORAGE_BUCKET_NAME: ${AWS_STORAGE_BUCKET_NAME}
+ AWS_REGION: ${AWS_REGION}
+ DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL}
+ EMAIL_HOST_USER: ${EMAIL_HOST_USER}
+ EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
RABBITMQ_URL: amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq
@@ -121,6 +156,7 @@ services:
condition: service_healthy
telemetry:
condition: service_healthy
+ restart: unless-stopped
# Auth database
auth_postgres:
@@ -143,11 +179,10 @@ services:
# Auth service
auth:
container_name: auth_service
- build:
- context: .
- dockerfile: ./auth-service/Dockerfile
+ image: ghcr.io/space-df/auth-service:latest
+ pull_policy: always
environment:
- ENV: dev
+ ENV: ${ENV}
SECRET_KEY: ${AUTH_SECRET_KEY}
DB_NAME: auth_service
DB_USERNAME: postgres
@@ -183,14 +218,15 @@ services:
auth_postgres:
condition: service_healthy
rabbitmq:
- condition: service_started
+ condition: service_healthy
+ restart: unless-stopped
healthcheck:
test:
["CMD-SHELL", "curl -fsS http://localhost/auth/api/health || exit 1"]
interval: 5s
timeout: 5s
retries: 5
- start_period: 60s
+ start_period: 120s
# Dashboard database
dashboard_postgres:
@@ -213,11 +249,10 @@ services:
# Dashboard service
dashboard:
container_name: dashboard_service
- build:
- context: .
- dockerfile: ./dashboard-service/Dockerfile
+ image: ghcr.io/space-df/dashboard-service:latest
+ pull_policy: always
environment:
- ENV: dev
+ ENV: ${ENV}
SECRET_KEY: ${DASHBOARD_SECRET_KEY}
DB_NAME: dashboard_service
DB_USERNAME: postgres
@@ -236,7 +271,8 @@ services:
dashboard_postgres:
condition: service_healthy
rabbitmq:
- condition: service_started
+ condition: service_healthy
+ restart: unless-stopped
healthcheck:
test:
[
@@ -246,7 +282,7 @@ services:
interval: 5s
timeout: 5s
retries: 5
- start_period: 60s
+ start_period: 120s
# Device database
device_postgres:
@@ -269,11 +305,10 @@ services:
# Device service
device:
container_name: device_service
- build:
- context: .
- dockerfile: ./device-service/Dockerfile
+ image: ghcr.io/space-df/device-service:latest
+ pull_policy: always
environment:
- ENV: dev
+ ENV: ${ENV}
SECRET_KEY: ${DEVICE_SECRET_KEY}
DB_NAME: device_service
DB_USERNAME: postgres
@@ -296,21 +331,21 @@ services:
device_postgres:
condition: service_healthy
rabbitmq:
- condition: service_started
+ condition: service_healthy
+ restart: unless-stopped
healthcheck:
test:
["CMD-SHELL", "curl -fsS http://localhost/device/api/health || exit 1"]
interval: 5s
timeout: 5s
retries: 5
- start_period: 60s
+ start_period: 120s
# Transformer service (Go) - RabbitMQ Consumer
transformer:
container_name: transformer_service
- build:
- context: ./transformer-service
- dockerfile: Dockerfile
+ image: ghcr.io/space-df/transformer-service:latest
+ pull_policy: always
hostname: transformer
environment:
- AMQP_BROKER_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/
@@ -334,7 +369,7 @@ services:
- DEVICE_CACHE_REDIS_DIAL_TIMEOUT_MS=2000
depends_on:
rabbitmq:
- condition: service_started
+ condition: service_healthy
emqx:
condition: service_started
links:
@@ -345,47 +380,11 @@ services:
- ./transformer-service:/transformer-service
- transformer-logs:/app/logs
- # Logrotate service for transformer logs
- transformer-logrotate:
- container_name: transformer_logrotate
- image: alpine:3.18
- hostname: transformer-logrotate
- environment:
- - TZ=UTC
- volumes:
- - transformer-logs:/app/logs
- - ./transformer-service/logrotate.conf:/etc/logrotate.d/transformer:ro
- command: >
- sh -c "
- apk add --no-cache logrotate dcron &&
- echo '0 0 * * * /usr/sbin/logrotate -f /etc/logrotate.d/transformer' > /etc/crontabs/root &&
- crond -f -L /dev/stdout
- "
- restart: unless-stopped
- privileged: true
- depends_on:
- - transformer
-
- # Transformer logs HTTP server for auto-loading logs in visualization tool
- transformer-logs-server:
- image: python:3.12-alpine
- container_name: transformer_logs_server
- working_dir: /srv
- command: python3 -m http.server 8081
- volumes:
- - ./transformer-service:/srv:ro
- ports:
- - "8081:8081"
- restart: unless-stopped
- depends_on:
- - transformer
-
# Broker Bridge Service - AMQP to EMQX MQTT bridge
broker-bridge:
container_name: broker_bridge_service
- build:
- context: ./broker-bridge-service
- dockerfile: Dockerfile
+ image: ghcr.io/space-df/broker-bridge-service:latest
+ pull_policy: always
hostname: broker-bridge
environment:
- MQTT_BROKER=emqx
@@ -418,16 +417,22 @@ services:
build:
context: .
dockerfile: ./haproxy/Dockerfile
- args:
- HOST: ${HOST}
- JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
+ networks:
+ default:
+ aliases:
+ - ${ORG_SLUG}.haproxy
+ environment:
+ HOST: ${HOST}
+ JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
hostname: haproxy
ports:
- - "3000:3000"
+ - "8000:3000"
- "443:443"
- "8884:8884"
- "8883:8883"
depends_on:
+ bootstrap:
+ condition: service_started
auth:
condition: service_started
dashboard:
@@ -441,14 +446,16 @@ services:
docs:
condition: service_started
links:
+ - bootstrap
- auth
- dashboard
- device
- telemetry
- mpa
- docs
+ restart: unless-stopped
volumes:
- - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
+ - ./haproxy/config:/usr/local/etc/haproxy/config
- ./haproxy/handlers:/usr/local/etc/haproxy/handlers
- ./haproxy/routes:/usr/local/share/lua/5.4/routes
- ./haproxy/certs:/usr/local/etc/haproxy/certs:ro
@@ -467,9 +474,8 @@ services:
# MPA Service
mpa:
container_name: mpa_service
- build:
- context: ./mpa-service
- dockerfile: Dockerfile
+ image: ghcr.io/space-df/mpa-service:latest
+ pull_policy: always
hostname: mpa
environment:
- MQTT_BROKER=${MQTT_BROKER}
@@ -491,7 +497,7 @@ services:
- rabbitmq
restart: unless-stopped
healthcheck:
- test: "curl --silent --fail http://localhost:80/health > /dev/null || exit 1"
+ test: "curl --silent --fail http://localhost/health > /dev/null || exit 1"
interval: 30s
timeout: 10s
retries: 3
@@ -516,13 +522,12 @@ services:
retries: 20
restart: unless-stopped
- # Telemetry Service - Consumes device telemetry data and stores in TimescaleDB
+ # Telemetry Service
telemetry:
container_name: telemetry_service
hostname: telemetry
- build:
- context: ./telemetry-service
- dockerfile: Dockerfile
+ image: ghcr.io/space-df/telemetry-service:latest
+ pull_policy: always
environment:
- AMQP_BROKER_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672/
- DB_NAME=spacedf_telemetry
@@ -534,7 +539,7 @@ services:
- "8080:8080" # API port
depends_on:
rabbitmq:
- condition: service_started
+ condition: service_healthy
timescaledb:
condition: service_healthy
restart: unless-stopped
@@ -551,7 +556,7 @@ services:
interval: 30s
timeout: 10s
retries: 3
- start_period: 30s
+ start_period: 120s
docs:
build:
@@ -573,6 +578,46 @@ services:
volumes:
- ./docs:/docs
+ # Web Application
+ web:
+ image: ghcr.io/space-df/spacedf-web-app:latest
+ pull_policy: always
+ platform: linux/amd64
+ environment:
+ NEXTAUTH_URL: ${DASHBOARD_NEXTAUTH_URL}
+ MAPTILER_API_KEY: ${MAPTILER_API_KEY}
+ NEXTAUTH_SECRET: ${DASHBOARD_NEXTAUTH_SECRET}
+ AUTH_API: ${ASHBOARD_AUTH_API}
+ DASHBOARD_MQTT_USERNAME: ${DASHBOARD_MQTT_USERNAME}
+ DASHBOARD_MQTT_PASSWORD: ${DASHBOARD_MQTT_PASSWORD}
+ DASHBOARD_MQTT_PORT: ${DASHBOARD_MQTT_PORT}
+ DASHBOARD_MQTT_PROTOCOL: ${DASHBOARD_MQTT_PROTOCOL}
+ DASHBOARD_MQTT_BROKER: ${DASHBOARD_MQTT_BROKER}
+ DEBUG: "True"
+ ports:
+ - "3000:3000"
+ depends_on:
+ haproxy:
+ condition: service_started
+ restart: unless-stopped
+
+ # Admin Portal Application
+ auth-portal:
+ image: ghcr.io/space-df/spacedf-admin-portal:latest
+ pull_policy: always
+ platform: linux/amd64
+ environment:
+ NEXTAUTH_URL: ${PORTAL_NEXTAUTH_URL}
+ NEXTAUTH_SECRET: ${PORTAL_NEXTAUTH_SECRET}
+ AUTH_API: ${PORTAL_AUTH_API}
+ DEBUG: "True"
+ ports:
+ - "3001:3001"
+ depends_on:
+ haproxy:
+ condition: service_started
+ restart: unless-stopped
+
volumes:
auth-postgres-vlm:
dashboard-postgres-vlm:
@@ -584,3 +629,4 @@ volumes:
transformer-logs:
rabbitmq_data:
timescaledb-data:
+ bootstrap-postgres-vlm:
diff --git a/docs/swagger-server.js b/docs/swagger-server.js
index 7a27295..9c6c0d8 100644
--- a/docs/swagger-server.js
+++ b/docs/swagger-server.js
@@ -6,7 +6,7 @@ const app = express();
const port = 3000;
const openApiUrls = [
- 'http://console/console/docs/?format=openapi',
+ 'http://bootstrap/bootstrap/docs/?format=openapi',
'http://auth/auth/docs/?format=openapi',
'http://dashboard/dashboard/docs/?format=openapi',
'http://device/device/docs/?format=openapi',
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100755
index 0000000..161a701
--- /dev/null
+++ b/entrypoint.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+set -a
+source .env
+set +a
+
+API_URL="${HOST}/docs"
+HOST_FRONTEND_ADMIN="${HOST_FRONTEND_ADMIN}"
+SCHEME="$(echo "$DASHBOARD_NEXTAUTH_URL" | sed -E 's#(https?://).*#\1#')"
+DOMAIN="$(echo "$DASHBOARD_NEXTAUTH_URL" | sed -E 's#https?://##')"
+HOST_FRONTEND="${SCHEME}${ORG_SLUG}.${DOMAIN}"
+
+clear
+echo -e "\033[38;5;208m"
+cat <<'EOF'
+ _____ ____ ____ ______ ______ ____ ______
+ / ___/ / __ \ / | / ____/ / ____/ / __ \ / ____/
+ \__ \ / /_/ / / /| | / / / __/ / / / / / /_
+ ___/ / / ____/ / /_| | / /___ / /___ / /_/ / / __/
+/____/ /_/ /_/ |_| \____/ \____/ /_____/ /_/
+EOF
+echo -e "\033[0m"
+echo -e "\033[1;36mSpaceDF Core\033[0m"
+echo -e "\033[0;36mCloud Native Platform\033[0m"
+echo -e "Version: 0.0.1"
+echo
+
+# ===== Build & Start =====
+COMPOSE_FILE="./docker-compose.yml"
+PROJECT_NAME="spacedf-core"
+echo "Deploying SpaceDF Core..."
+echo "Stopping existing services (if running)..."
+docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" stop || true
+echo "Building images & starting services..."
+docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d --build --remove-orphans
+sleep 10
+
+# ===== Success Message =====
+echo
+echo -e "--------------------------------------------------"
+echo -e "SpaceDF Core started successfully"
+echo "🌐 Frontend Admin : ${HOST_FRONTEND_ADMIN}"
+echo "🌐 Frontend Dashboard : ${HOST_FRONTEND}"
+echo "🔗 Backend API : ${API_URL}"
\ No newline at end of file
diff --git a/generate-keys.sh b/generate-keys.sh
new file mode 100755
index 0000000..2c7773d
--- /dev/null
+++ b/generate-keys.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+set -e
+[ -f .env ] || { echo "Error: .env not found"; exit 1; }
+mkdir -p ./keys
+openssl genrsa -out ./keys/private_key.pem 2048 2>/dev/null
+openssl rsa -in ./keys/private_key.pem -pubout -out ./keys/public_key.pem 2>/dev/null
+P=$(cat ./keys/private_key.pem | sed 's/$/\\n/' | tr -d '\n' | sed 's/\\n$//')
+U=$(cat ./keys/public_key.pem | sed 's/$/\\n/' | tr -d '\n' | sed 's/\\n$//')
+export P U PS=$(openssl rand -base64 32) DS=$(openssl rand -base64 32) RP=$(openssl rand -base64 16) OP=$(openssl rand -base64 16) BSK=$(openssl rand -base64 32) DSK=$(openssl rand -base64 32) DBSK=$(openssl rand -base64 32)
+python3 << 'EOF'
+import os, re
+
+c = open('.env').read()
+c = re.sub(r'^JWT_PRIVATE_KEY=.*$', '', c, flags=re.M)
+c = re.sub(r'^JWT_PUBLIC_KEY=.*$', '', c, flags=re.M)
+c = re.sub(
+ r'(# Authentication JWT\n)',
+ f'\\1JWT_PRIVATE_KEY="{os.environ["P"]}"\nJWT_PUBLIC_KEY="{os.environ["U"]}"\n',
+ c
+)
+
+env_vars = {
+ 'RABBITMQ_DEFAULT_USER': 'default',
+ 'AUTH_POSTGRES_PASSWORD': 'postgres',
+ 'REDIS_HOST': 'redis://redis:6379/1',
+ 'DASHBOARD_POSTGRES_PASSWORD': 'postgres',
+ 'DEVICE_POSTGRES_PASSWORD': 'postgres',
+ 'BOOTSTRAP_POSTGRES_PASSWORD': 'postgres',
+ 'EMQX_USERNAME': 'user',
+ 'EMQX_PASSWORD': 'password123',
+ 'MQTT_BROKER_BRIDGE_PASSWORD': 'Default@1234',
+ 'MQTT_PASSWORD': 'Default@1234',
+ 'ORG_NAME': 'Default Organization',
+ 'ORG_SLUG': 'default-org',
+ 'OWNER_EMAIL': 'admin@example.com',
+ 'OWNER_PASSWORD': 'changeme@Default123',
+ 'BOOTSTRAP_SECRET_KEY': os.environ['BSK'],
+ 'DEVICE_SECRET_KEY': os.environ['DSK'],
+ 'DASHBOARD_SECRET_KEY': os.environ['DBSK'],
+ 'DASHBOARD_MQTT_USERNAME': 'anonymous',
+ 'DASHBOARD_MQTT_PASSWORD': 'password123',
+ 'RABBITMQ_DEFAULT_PASS': 'password',
+ 'PORTAL_NEXTAUTH_SECRET': os.environ['PS'],
+ 'DASHBOARD_NEXTAUTH_SECRET': os.environ['DS']
+}
+
+for key, value in env_vars.items():
+ if f'{key}=' in c:
+ c = re.sub(f'^{key}=.*$', f'{key}="{value}"', c, flags=re.M)
+ else:
+ c += f'\n{key}="{value}"'
+with open('.env', 'w') as f:
+ f.write(c)
+EOF
+rm -rf ./keys
+echo "✓ Done"
diff --git a/haproxy b/haproxy
deleted file mode 160000
index d639316..0000000
--- a/haproxy
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d639316cd0a3d9b0f1db85c1aec6ace363f97e1b
diff --git a/haproxy/CLA.md b/haproxy/CLA.md
new file mode 100644
index 0000000..093c6b7
--- /dev/null
+++ b/haproxy/CLA.md
@@ -0,0 +1,67 @@
+# Contributor License Agreement (CLA)
+
+**Version 1.0**
+
+This Contributor License Agreement (“**Agreement**”) is entered into by **you** (“**Contributor**”) and **Digital Fortress** (“**Company**”) regarding your contributions to the **SpaceDF** project (“**Project**”).
+
+By submitting any Contribution to the Project, you agree to the following terms:
+
+## 1. Definitions
+
+- **“Contribution”** means any source code, documentation, design, or other material submitted by you to the Project.
+- **“Submit”** means any form of electronic, written, or verbal communication intended to be included in the Project, including but not limited to pull requests, patches, issues, or comments.
+
+## 2. Copyright Ownership
+
+- You retain ownership of the copyright in your Contributions.
+- Nothing in this Agreement transfers ownership of your intellectual property to the Company.
+
+## 3. License Grant
+
+You grant **Digital Fortress** a **perpetual, worldwide, non-exclusive, royalty-free, and irrevocable license** to:
+
+- Use
+- Modify
+- Distribute
+- Re-license
+- Sublicense
+- Commercialize
+
+your Contributions as part of the Project or in any related products or services.
+
+This includes, but is not limited to, use in **proprietary**, **SaaS**, and **enterprise** offerings.
+
+## 4. Patent Grant
+
+You grant Digital Fortress a **perpetual, worldwide, royalty-free license** to any patent claims you own that are necessarily infringed by your Contributions.
+
+## 5. Representations
+
+You represent and warrant that:
+
+- You have the legal right to submit the Contributions.
+- The Contributions do not violate or infringe upon any third-party rights.
+- If your employer or organization has intellectual property policies, you have obtained all necessary permissions to make the Contributions.
+
+## 6. No Obligation
+
+The Company is **not obligated** to:
+
+- Accept your Contributions.
+- Provide any form of compensation.
+- Include your Contributions in any release or distribution.
+
+## 7. Public Attribution
+
+The Company **may**, but is not required to, publicly acknowledge or attribute your Contributions.
+
+## 8. License Compatibility
+
+- Your Contributions will be licensed to users under the Project’s open-source license (e.g., **Apache License 2.0**).
+- This Agreement governs only the relationship between you and the Company and does not modify the Project’s open-source license.
+
+## 9. Governing Law
+
+This Agreement shall be governed by and construed in accordance with the laws of **Vietnam**.
+
+By submitting a Contribution, you confirm that you have read, understood, and agree to the terms of this Agreement.
diff --git a/haproxy/Dockerfile b/haproxy/Dockerfile
new file mode 100644
index 0000000..05cd651
--- /dev/null
+++ b/haproxy/Dockerfile
@@ -0,0 +1,28 @@
+FROM haproxy:3.1.5
+
+USER root
+
+# Install Lua dependencies
+RUN apt-get update && apt-get install -y lua5.4 lua-cjson lua-socket git \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install redis-lua
+RUN mkdir -p /usr/local/share/lua/5.4/ && \
+ git clone https://github.com/nrk/redis-lua.git /tmp/redis-lua && \
+ mv /tmp/redis-lua/src/redis.lua /usr/local/share/lua/5.4/redis.lua && \
+ rm -rf /tmp/redis-lua
+
+# Copy config and scripts
+COPY haproxy/haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
+COPY haproxy/handlers /usr/local/etc/haproxy/handlers
+COPY haproxy/routes /usr/local/share/lua/5.4/routes
+
+# Copy entrypoint
+COPY haproxy/docker-entrypoint.sh /docker-entrypoint.sh
+RUN chmod +x /docker-entrypoint.sh
+RUN chown -R haproxy:haproxy /usr/local/etc/haproxy
+
+# Run the production server
+ENTRYPOINT ["/docker-entrypoint.sh"]
+
+USER haproxy
\ No newline at end of file
diff --git a/haproxy/LICENSE b/haproxy/LICENSE
new file mode 100644
index 0000000..10398cd
--- /dev/null
+++ b/haproxy/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2026 Digital Fortress
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/haproxy/README.md b/haproxy/README.md
new file mode 100644
index 0000000..2d1f642
--- /dev/null
+++ b/haproxy/README.md
@@ -0,0 +1,35 @@
+# HAProxy for SpaceDF
+
+## Usage
+
+Clone the repository
+
+```sh
+git clone --recurse-submodules git@github.com:Space-DF/spacedf-backend.git
+```
+
+Run Docker containers
+
+```sh
+cd spacedf-backend
+docker-compose up
+```
+
+Set up SSL/TSL for EMQX
+
+```sh
+sudo apt install certbot -y
+## NOTE: Verify there are no applications running on port 80, 443
+sudo certbot certonly -d emqx.example.com
+sudo cp /etc/letsencrypt/live/emqx.example.com/fullchain.pem ./spacedf-backend/haproxy/certs
+sudo cp /etc/letsencrypt/live/emqx.example.com/privkey.pem ./spacedf-backend/haproxy/certs
+sudo chown root:root ./spacedf-backend/haproxy/certs/*.pem
+sudo chmod 600 ./spacedf-backend/haproxy/certs/privkey.pem
+cd spacedf-backend/haproxy/certs && cat fullchain.pem privkey.pem > emqx.pem
+```
+
+## License
+Licensed under the Apache License, Version 2.0
+See the LICENSE file for details.
+
+[](https://df.technology/)
diff --git a/haproxy/docker-entrypoint.sh b/haproxy/docker-entrypoint.sh
new file mode 100644
index 0000000..b7031aa
--- /dev/null
+++ b/haproxy/docker-entrypoint.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -e
+
+echo "$JWT_PUBLIC_KEY" > /usr/local/etc/haproxy/pubkey.pem
+exec haproxy -W -db -f /usr/local/etc/haproxy/haproxy.cfg
\ No newline at end of file
diff --git a/haproxy/handlers/http_handler.lua b/haproxy/handlers/http_handler.lua
new file mode 100644
index 0000000..8ff7142
--- /dev/null
+++ b/haproxy/handlers/http_handler.lua
@@ -0,0 +1,268 @@
+local routes = require('routes.routes')
+local cjson = require("cjson")
+local mime = require("mime")
+local redis = require('redis')
+
+local ROLE_HIERARCHY = { Viewer = 1, Editor = 2, Admin = 3, Owner = 4}
+
+local function redis_get(key)
+ local red, err = redis.connect("redis", 6379)
+ if not red then
+ return nil
+ end
+
+ red:select(1)
+
+ local ok, result = pcall(function()
+ return red:get(key)
+ end)
+
+ pcall(function()
+ red:quit()
+ end)
+
+ if not ok then
+ return nil
+ end
+
+ return result
+end
+
+function get_route(txn)
+ local path = txn.sf:path()
+ local method = txn.sf:method()
+
+ for route, methods in pairs(routes) do
+ if string.match(path, route) then
+ local api_info = methods[method]
+ if api_info then
+ return route
+ end
+ end
+ end
+ return "not_found"
+end
+
+local function get_route_info(txn, key, default)
+ local route = txn:get_var("txn.route")
+ if not route or route == "not_found" then
+ return default
+ end
+ return routes[route][txn.sf:method()][key]
+end
+
+function get_service(txn)
+ return get_route_info(txn, "service", "not_found")
+end
+
+function get_auth_required(txn)
+ return get_route_info(txn, "auth_required", false)
+end
+
+local function validate_entity(txn, entity_name)
+ local entity_required = get_route_info(txn, entity_name .. "_required", false)
+ if entity_required then
+ local entity = txn:get_var("txn." .. entity_name)
+ if not entity then
+ return false
+ end
+
+ txn.http:req_set_header("x-" .. entity_name, entity)
+ end
+
+ return true
+end
+
+function validate_space(txn)
+ return validate_entity(txn, "space")
+end
+
+function validate_organization(txn)
+ return validate_entity(txn, "organization")
+end
+
+function check_change_roles(txn)
+ local space_required = get_route_info(txn, "space_required")
+ local org_required = get_route_info(txn, "organization_required")
+ if not space_required and not org_required then
+ return true
+ end
+ if space_required then
+ local space_roles = redis_get(":1:space_roles_" .. txn:get_var("txn.user_id"))
+ if not space_roles then
+ return false
+ end
+ return true
+ end
+
+ local org_roles = redis_get(":1:organization_roles_" .. txn:get_var("txn.user_id"))
+ if not org_roles then
+ return false
+ end
+ return true
+end
+
+function check_scope_roles(roles_json, data, required_role)
+ local status, roles_table = pcall(cjson.decode, roles_json)
+ if not status or not roles_table then
+ return false
+ end
+
+ local user_role = roles_table[data]
+ if not user_role then
+ return false
+ end
+ local user_rank = ROLE_HIERARCHY[user_role]
+ local required_rank = ROLE_HIERARCHY[required_role]
+
+ if not user_rank or not required_rank then
+ return false
+ end
+
+ if user_rank >= required_rank then
+ return true
+ end
+ return false
+end
+
+function check_roles(txn)
+ local required_roles = get_route_info(txn, "role_required", nil)
+ local check_roles_org = get_route_info(txn, "organization_required", nil)
+ local check_roles_space = get_route_info(txn, "space_required", nil)
+
+ if not required_roles then
+ return true
+ end
+
+ if check_roles_space then
+ local roles_json = txn:get_var("txn.space_roles")
+ local space = txn:get_var("txn.space")
+ local result = check_scope_roles(roles_json, space, required_roles)
+ if result ~= false then return result end
+ end
+
+ if check_roles_org then
+ local roles_json = txn:get_var("txn.organization_roles")
+ local org = txn:get_var("txn.organization")
+ local result = check_scope_roles(roles_json, org, required_roles)
+ if result ~= false then return result end
+ end
+ return false
+end
+
+function validate_issuer(txn)
+ local token_issuer = txn:get_var("txn.iss")
+ if not token_issuer then
+ return false
+ end
+
+ local is_root_user_api = get_route_info(txn, "is_root_user_api", false)
+ if is_root_user_api then
+ local host = os.getenv("HOST")
+ if not host then
+ return false
+ end
+
+ return token_issuer == host
+ end
+
+ if token_issuer:match("^https?://([^/]+)") then
+ return true
+ end
+
+ return false
+end
+
+local function split_jwt(token)
+ local parts = {}
+ for part in string.gmatch(token, "[^.]+") do
+ table.insert(parts, part)
+ end
+ return parts[1], parts[2], parts[3]
+end
+
+local function decode_jwt_payload(jwt)
+ local _, payload_b64, _ = split_jwt(jwt)
+ if not payload_b64 then
+ return nil
+ end
+
+ local b64 = payload_b64:gsub('-', '+'):gsub('_', '/')
+ local pad = #b64 % 4
+ if pad > 0 then b64 = b64 .. string.rep("=", 4 - pad) end
+ local decoded_json = mime.unb64(b64)
+ if not decoded_json then
+ return nil
+ end
+
+ local ok, result = pcall(cjson.decode, decoded_json)
+ if not ok then
+ return nil
+ end
+ return result
+end
+
+function decode_jwt(txn)
+ local auth_header = txn.sf:req_hdr("Authorization")
+ if not auth_header then return end
+ local jwt = auth_header:gsub("Bearer ", "")
+ local payload = decode_jwt_payload(jwt)
+ if payload and payload["space_roles"] then
+ txn:set_var("txn.space_roles", cjson.encode(payload["space_roles"]))
+ end
+ if payload and payload["organization_roles"] then
+ txn:set_var("txn.organization_roles", cjson.encode(payload["organization_roles"]))
+ end
+end
+
+local function split_jwt(token)
+ local parts = {}
+ for part in string.gmatch(token, "[^.]+") do
+ table.insert(parts, part)
+ end
+ return parts[1], parts[2], parts[3]
+end
+
+local function decode_jwt_payload(jwt)
+ local _, payload_b64, _ = split_jwt(jwt)
+ if not payload_b64 then
+ return nil
+ end
+
+ local b64 = payload_b64:gsub('-', '+'):gsub('_', '/')
+ local pad = #b64 % 4
+ if pad > 0 then b64 = b64 .. string.rep("=", 4 - pad) end
+ local decoded_json = mime.unb64(b64)
+ if not decoded_json then
+ return nil
+ end
+
+ local ok, result = pcall(cjson.decode, decoded_json)
+ if not ok then
+ return nil
+ end
+ return result
+end
+
+function decode_jwt(txn)
+ local auth_header = txn.sf:req_hdr("Authorization")
+ if not auth_header then return end
+ local jwt = auth_header:gsub("Bearer ", "")
+ local payload = decode_jwt_payload(jwt)
+ if payload and payload["space_roles"] then
+ txn:set_var("txn.space_roles", cjson.encode(payload["space_roles"]))
+ end
+ if payload and payload["organization_roles"] then
+ txn:set_var("txn.organization_roles", cjson.encode(payload["organization_roles"]))
+ end
+end
+
+core.register_fetches("get_route", get_route)
+core.register_fetches("get_service", get_service)
+core.register_fetches("get_auth_required", get_auth_required)
+core.register_fetches("check_change_roles", check_change_roles)
+core.register_fetches("check_roles", check_roles)
+core.register_fetches("validate_space", validate_space)
+core.register_fetches("validate_organization", validate_organization)
+core.register_fetches("validate_issuer", validate_issuer)
+core.register_action("decode_jwt", { "http-req" }, decode_jwt)
\ No newline at end of file
diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg
new file mode 100644
index 0000000..16c074e
--- /dev/null
+++ b/haproxy/haproxy.cfg
@@ -0,0 +1,152 @@
+global
+ daemon
+ log 127.0.0.1 local0
+ log 127.0.0.1 local1 notice
+ maxconn 4096
+ tune.ssl.default-dh-param 2048
+ lua-load /usr/local/etc/haproxy/handlers/http_handler.lua
+
+defaults
+ log global
+ retries 3
+ maxconn 2000
+ timeout connect 5s
+ timeout client 50s
+ timeout server 50s
+
+
+resolvers docker
+ nameserver dns 127.0.0.11:53
+ resolve_retries 3
+ timeout retry 1s
+ hold valid 10s
+
+# ================================
+# EMQX TLS termination (TCP)
+# ================================
+frontend ws_frontend
+ bind *:8883
+ mode tcp
+ default_backend mqtt_ws_backend
+
+backend mqtt_ws_backend
+ mode tcp
+ balance roundrobin
+ server emqx1 emqx:8083 check
+
+backend mqtt_wss_backend
+ mode tcp
+ balance roundrobin
+ server emqx1 emqx:8083 check
+
+# ================================
+# HTTP Frontend and Backends
+# ================================
+frontend request_front
+ bind *:3000
+ mode http
+ option httplog
+ option forwardfor
+
+ # Set headers
+ http-request set-header Host %[req.hdr(Host),field(1,:)]
+ http-request set-header Host %[req.hdr(Host),regsub(\.haproxy$,\.localhost)]
+ http-request set-header X-Forwarded-Host %[req.hdr(Host)]
+ http-request set-header X-Forwarded-Proto http
+
+ # WebSocket handling
+ acl is_websocket hdr(Upgrade) -i WebSocket
+ acl is_upgrade hdr(Connection) -i upgrade
+
+ # Get route
+ http-request set-var(txn.route) lua.get_route
+
+ # ------------------------------
+ # JWT Validation
+ # ------------------------------
+ # Check if need to validate JWT
+ http-request allow if !{ lua.get_auth_required -m bool }
+
+ # Ensure JWT has Authorization header
+ http-request deny unless { req.hdr(authorization) -m found }
+
+ # Get payload part of the JWT
+ http-request lua.decode_jwt
+ http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
+ http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
+ http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
+ http-request set-var(txn.user_id) http_auth_bearer,jwt_payload_query('$.user_id')
+ http-request set-var(txn.space) req.hdr(X-Space)
+ http-request set-var(txn.organization) req.hdr(X-Organization)
+
+ # Validate the JWT
+ http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
+
+ # Validate the JWT signature
+ http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/usr/local/etc/haproxy/pubkey.pem") -m int 1 }
+
+ # Check expired the JWTxx
+ http-request set-var(txn.now) date()
+ http-request deny status 401 content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
+
+ # Check issuer
+ http-request deny content-type 'text/html' string 'Invalid issuer' unless { lua.validate_issuer -m bool }
+
+ # Set user ID header
+ http-request set-header X-User-ID %[var(txn.user_id)]
+
+ # Validate space
+ http-request deny content-type 'text/html' string 'Invalid space' unless { lua.validate_space -m bool }
+
+ # Validate organization
+ http-request deny content-type 'text/html' string 'Invalid organization' unless { lua.validate_organization -m bool }
+
+ # Get roles for root user
+ http-request set-var(txn.role_required) http_auth_bearer,jwt_payload_query('$.role_required')
+
+ # Check change roles
+ http-request deny status 401 content-type 'text/html' string 'Changed roles.' unless { lua.check_change_roles -m bool }
+
+ # Check roles
+ http-request deny content-type 'text/html' string 'Insufficient roles' unless { lua.check_roles -m bool }
+
+ # --------------------------------
+ # Route
+ # --------------------------------
+ # Default routing for non-WebSocket traffic
+ use_backend %[lua.get_service]_backend
+
+backend bootstrap_backend
+ mode http
+ balance roundrobin
+ server local bootstrap:80
+
+backend auth_backend
+ mode http
+ server local auth:80
+
+backend dashboard_backend
+ mode http
+ server local dashboard:80
+
+backend device_backend
+ mode http
+ server local device:80
+
+backend telemetry_backend
+ mode http
+ server local telemetry:8080
+
+backend docs_backend
+ mode http
+ server local docs:3000
+
+backend mpa_backend
+ mode http
+ option http-server-close
+ option forwardfor
+ server local mpa:80
+
+backend not_found_backend
+ mode http
+ http-request deny deny_status 404
diff --git a/haproxy/routes/auth.lua b/haproxy/routes/auth.lua
new file mode 100644
index 0000000..657d64b
--- /dev/null
+++ b/haproxy/routes/auth.lua
@@ -0,0 +1,377 @@
+routes = {
+ -- --------------
+ -- Auth
+ -- --------------
+ ["^/api/auth/login/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/send%-otp/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/send%-email%-confirm/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/google/login/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/change%-password/?$"] = {
+ PUT = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/forget%-password/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/users/me/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/presigned%-url/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/presigned%-url/.*$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/oauth2/google/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/oauth2/spacedf%-console/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/register/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/refresh%-token/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/auth/spaces/switch/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/spaces/invitation/?$"] = {
+ POST = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/join%-space/.*$"] = {
+ GET = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/spaces/join%-space/.*$"] = {
+ GET = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/credentials/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ -- --------------
+ -- Space
+ -- --------------
+ ["^/api/spaces/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ POST = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ -- --------------
+ -- Space Roles & Policies
+ -- --------------
+ ["^/api/space%-policies/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/space%-policies/[0-9a-f-]+/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = nil,
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/space%-role%-users/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/space%-role%-users/.*$"] = {
+ POST = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/space%-role%-users/[0-9a-f-]+/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/space%-roles/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ POST = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/space%-roles/[0-9a-f-]+/?$"] = {
+ GET = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "auth",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+}
+
+return routes
diff --git a/haproxy/routes/bootstrap.lua b/haproxy/routes/bootstrap.lua
new file mode 100644
index 0000000..1516c55
--- /dev/null
+++ b/haproxy/routes/bootstrap.lua
@@ -0,0 +1,156 @@
+routes = {
+ -- --------------
+ -- Bootstrap
+ -- --------------
+ ["^/api/bootstrap/auth/login/?$"] = {
+ POST = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ },
+ ["^/static/images/.*$"] = {
+ GET = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/bootstrap/auth/send%-email%-confirm/?$"] = {
+ POST = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/bootstrap/auth/forget%-password/?$"] = {
+ POST = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/bootstrap/user/me/?$"] = {
+ GET = {
+ service = "bootstrap",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ PUT = {
+ service = "bootstrap",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ PATCH = {
+ service = "bootstrap",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/bootstrap/auth/register/?$"] = {
+ POST = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/organizations/check/.*$"] = {
+ GET = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/bootstrap/auth/refresh%-token/?$"] = {
+ POST = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/bootstrap/auth/tokens/?$"] = {
+ GET = {
+ service = "bootstrap",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ },
+ -- --------------
+ -- Organizations
+ -- --------------
+ ["^/api/organizations/?$"] = {
+ GET = {
+ service = "bootstrap",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/bootstrap/auth/change%-password/?$"] = {
+ PUT = {
+ service = "bootstrap",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/bootstrap/presigned%-url/?$"] = {
+ GET = {
+ service = "bootstrap",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/bootstrap/presigned%-url/.*$"] = {
+ GET = {
+ service = "bootstrap",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+}
+
+return routes
diff --git a/haproxy/routes/dashboard.lua b/haproxy/routes/dashboard.lua
new file mode 100644
index 0000000..d96df4a
--- /dev/null
+++ b/haproxy/routes/dashboard.lua
@@ -0,0 +1,177 @@
+routes = {
+ -- --------------
+ -- Dashboard
+ -- --------------
+ ["^/api/dashboards/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ POST = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/dashboards/[0-9a-f-]+/widgets/bulk%-create/?$"] = {
+ POST = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/dashboards/[0-9a-f-]+/widgets/bulk%-update/?$"] = {
+ PUT = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/dashboards/[0-9a-f-]+/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ -- --------------
+ -- Widgets
+ -- --------------
+ ["^/api/dashboards/[0-9a-f-]+/widgets/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ POST = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/dashboards/[0-9a-f-]+/widgets/[0-9a-f-]+/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ -- --------------
+ -- Device states
+ -- --------------
+ ["^/api/device%-states/daily/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/device%-states/hourly/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/device%-states/minutely/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/device%-states/monthly/?$"] = {
+ GET = {
+ service = "dashboard",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+}
+
+return routes
diff --git a/haproxy/routes/device.lua b/haproxy/routes/device.lua
new file mode 100644
index 0000000..b48184c
--- /dev/null
+++ b/haproxy/routes/device.lua
@@ -0,0 +1,330 @@
+routes = {
+ -- --------------
+ -- Device
+ -- --------------
+ ["^/api/device%-models/?$"] = {
+ POST = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/device%-models/[0-9a-f-]+/?$"] = {
+ PUT = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ PATCH = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ DELETE = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/device%-spaces/?$"] = {
+ POST = {
+ service = "device",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/device%-spaces/[0-9a-f-]+/?$"] = {
+ PUT = {
+ service = "device",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "device",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "device",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/devices/bulk%-create/?$"] = {
+ POST = {
+ service = "device",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/devices/?$"] = {
+ POST = {
+ service = "device",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/devices/[0-9a-f-]+/?$"] = {
+ PUT = {
+ service = "device",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ PATCH = {
+ service = "device",
+ auth_required = true,
+ role_required = "Editor",
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ DELETE = {
+ service = "device",
+ auth_required = true,
+ role_required = "Admin",
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/devices/[^/]+/check/?$"] = {
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/manufacturers/?$"] = {
+ POST = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/manufacturers/[0-9a-f-]+/?$"] = {
+ PUT = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ PATCH = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ DELETE = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/network%-server/?$"] = {
+ POST = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/network%-server/[0-9a-f-]+/?$"] = {
+ PUT = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ PATCH = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ DELETE = {
+ service = "device",
+ auth_required = true,
+ role_required = nil,
+ space_required = false,
+ organization_required = true,
+ is_root_user_api = true,
+ },
+ },
+ ["^/api/device%-transformed%-data/?$"] = {
+ GET = {
+ service = "device",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ }
+ },
+ ["^/api/device%-transformed%-data/[0-9a-f-]+/?$"] = {
+ GET = {
+ service = "device",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ }
+ },
+ -- --------------
+ -- Trips
+ -- --------------
+ ["^/api/trips/?$"] = {
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ }
+ },
+ ["^/api/trips/[0-9a-f-]+/?$"] = {
+ GET = {
+ service = "device",
+ auth_required = true,
+ role_required = "Viewer",
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+}
+
+return routes
\ No newline at end of file
diff --git a/haproxy/routes/docs.lua b/haproxy/routes/docs.lua
new file mode 100644
index 0000000..866576b
--- /dev/null
+++ b/haproxy/routes/docs.lua
@@ -0,0 +1,25 @@
+routes = {
+ -- --------------
+ -- API documentation
+ -- --------------
+ ["^/docs.*$"] = {
+ GET = {
+ service = "docs",
+ auth_required = false,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/openapi.json$"] = {
+ GET = {
+ service = "docs",
+ auth_required = false,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+}
+
+return routes
diff --git a/haproxy/routes/mpa.lua b/haproxy/routes/mpa.lua
new file mode 100644
index 0000000..d6a4452
--- /dev/null
+++ b/haproxy/routes/mpa.lua
@@ -0,0 +1,175 @@
+routes = {
+ -- --------------
+ -- MPA Service Routes
+ -- --------------
+ ["^/http/?.*$"] = {
+ GET = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ POST = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ OPTIONS = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/ws/?.*$"] = {
+ GET = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ POST = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ OPTIONS = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/socketio/?.*$"] = {
+ GET = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ POST = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PUT = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ PATCH = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ DELETE = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ OPTIONS = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/lorawan/[^/]+/[^/]+/?.*$"] = {
+ POST = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ OPTIONS = {
+ service = "mpa",
+ auth_required = false,
+ role_required = nil,
+ space_required = false,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+}
+
+return routes
\ No newline at end of file
diff --git a/haproxy/routes/routes.lua b/haproxy/routes/routes.lua
new file mode 100644
index 0000000..47773ba
--- /dev/null
+++ b/haproxy/routes/routes.lua
@@ -0,0 +1,18 @@
+docs_routes = require("routes.docs")
+auth_routes = require("routes.auth")
+dashboard_routes = require("routes.dashboard")
+device_routes = require("routes.device")
+mpa_routes = require("routes.mpa")
+telemetry_routes = require("routes.telemetry")
+bootstrap_routes = require("routes.bootstrap")
+
+routes = {}
+for k, v in pairs(docs_routes) do routes[k] = v end
+for k, v in pairs(auth_routes) do routes[k] = v end
+for k, v in pairs(dashboard_routes) do routes[k] = v end
+for k, v in pairs(device_routes) do routes[k] = v end
+for k, v in pairs(mpa_routes) do routes[k] = v end
+for k, v in pairs(telemetry_routes) do routes[k] = v end
+for k, v in pairs(bootstrap_routes) do routes[k] = v end
+
+return routes
diff --git a/haproxy/routes/telemetry.lua b/haproxy/routes/telemetry.lua
new file mode 100644
index 0000000..fa2f3c9
--- /dev/null
+++ b/haproxy/routes/telemetry.lua
@@ -0,0 +1,25 @@
+routes = {
+ -- --------------
+ -- Telemetry
+ -- --------------
+ ["^/api/telemetry/v1/entities/?$"] = {
+ GET = {
+ service = "telemetry",
+ auth_required = true,
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+ ["^/api/telemetry/v1/alerts/?$"] = {
+ GET = {
+ service = "telemetry",
+ auth_required = true,
+ space_required = true,
+ organization_required = false,
+ is_root_user_api = false,
+ },
+ },
+}
+
+return routes
diff --git a/mpa-service b/mpa-service
index 68576ef..3981984 160000
--- a/mpa-service
+++ b/mpa-service
@@ -1 +1 @@
-Subproject commit 68576ef65923d0bac1498c46007e34f6337210f3
+Subproject commit 39819848ddcfa43cd50b5b2a369b3a658c7bc84c
diff --git a/setup.cfg b/setup.cfg
index 01e8981..3916337 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,7 +4,7 @@ exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[isort]
line_length = 88
-known_first_party = console-service
+known_first_party = bootstrap-service
multi_line_output = 3
default_section = THIRDPARTY
skip = venv/
@@ -14,7 +14,7 @@ force_grid_wrap = 0
use_parentheses = true
[coverage:run]
-include = console-service/*
+include = bootstrap-service/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin
diff --git a/telemetry-service b/telemetry-service
index 46d07ae..1604c04 160000
--- a/telemetry-service
+++ b/telemetry-service
@@ -1 +1 @@
-Subproject commit 46d07aed1b625419d1d3e4060eb634b31f51a9e9
+Subproject commit 1604c041cedc24e6fcef439aadd25bc58633c905
diff --git a/transformer-service b/transformer-service
index 2b1e6cf..3a124d0 160000
--- a/transformer-service
+++ b/transformer-service
@@ -1 +1 @@
-Subproject commit 2b1e6cfe98035ce6da566bfcd0c70f781052ee90
+Subproject commit 3a124d01b826611c5999d217d16b704ea9fd473d