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 @@ -

- - - - Digital Fortress logo - - -

- ---- - -# 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 - - - - Digital Fortress logo - - - -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 +[![SpaceDF - A project from Digital Fortress](https://df.technology/images/SpaceDF.png)](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. + +[![SpaceDF - A project from Digital Fortress](https://df.technology/images/SpaceDF.png)](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