Compare commits

..

1 Commits

Author SHA1 Message Date
Ronald A. Richardson
62a69b2dcc Fullstack docker image 2025-05-01 08:55:26 +08:00
241 changed files with 7524 additions and 21632 deletions

View File

@@ -1,64 +0,0 @@
name: Build Fleetbase Binaries
on:
workflow_dispatch:
workflow_run:
workflows: ["Create Release"]
types: [completed]
permissions:
contents: write
env:
DIST_DIR: builds/dist
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build-linux:
name: Linux Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Linux binary
run: |
chmod +x ./builds/linux/build-linux.sh
./builds/linux/build-linux.sh
- name: Upload Linux binary
if: github.event_name == 'workflow_run'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.workflow_run.head_branch }}
files: |
${{ env.DIST_DIR }}/fleetbase-linux-x86_64
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos:
name: macOS (ARM64) Build
needs: build-linux
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install build dependencies
run: |
brew update
brew install autoconf automake coreutils asdf php@8.4
source "$(brew --prefix asdf)/libexec/asdf.sh"
asdf plugin add php https://github.com/asdf-community/asdf-php.git
- name: Build macOS binary
run: |
chmod +x ./builds/osx/build-osx.sh
./builds/osx/build-osx.sh
- name: Upload Linux binary
if: github.event_name == 'workflow_run'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.workflow_run.head_branch }}
files: |
${{ env.DIST_DIR }}/fleetbase-darwin-arm64
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -13,24 +13,9 @@ env:
GITHUB_AUTH_KEY: ${{ secrets._GITHUB_AUTH_TOKEN }}
jobs:
setup:
name: Setup Environment
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.extract-env.outputs.environment }}
steps:
- name: Extract environment from branch name
id: extract-env
run: |
ENVIRONMENT=$(echo "${{ github.ref_name }}" | sed 's|deploy/||')
echo "environment=${ENVIRONMENT}" >> $GITHUB_OUTPUT
echo "Deploying to environment: ${ENVIRONMENT}"
build_service:
name: Build and Deploy the Service
needs: [setup]
runs-on: ubuntu-latest
environment: ${{ needs.setup.outputs.environment }}
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
@@ -73,43 +58,6 @@ jobs:
files: |
./docker-bake.hcl
- name: Resolve ECS Targets
run: |
set -euo pipefail
# Detect naming scheme by checking if new cluster exists
NEW_CLUSTER="${PROJECT}-${STACK}-cluster"
if aws ecs describe-clusters --region "${AWS_REGION}" --clusters "${NEW_CLUSTER}" \
--query "clusters[?status=='ACTIVE'].clusterArn" --output text 2>/dev/null | grep -q .; then
# New scheme: use cluster suffix and service prefixes
CLUSTER="${NEW_CLUSTER}"
SERVICE_PREFIX="${PROJECT}-${STACK}-"
SERVICE_BASE="api"
else
# Legacy scheme: no suffixes/prefixes
CLUSTER="${PROJECT}-${STACK}"
SERVICE_PREFIX=""
SERVICE_BASE="app"
fi
# Build service names
API_SERVICE="${SERVICE_PREFIX}${SERVICE_BASE}"
SCHEDULER_SERVICE="${SERVICE_PREFIX}scheduler"
EVENTS_SERVICE="${SERVICE_PREFIX}events"
TASK_DEF="${PROJECT}-${STACK}-${SERVICE_BASE}"
# Get container name from task definition
CONTAINER_NAME="$(aws ecs describe-task-definition --task-definition "$TASK_DEF" \
--query 'taskDefinition.containerDefinitions[0].name' --output text 2>/dev/null || echo "$SERVICE_BASE")"
{
echo "CLUSTER=$CLUSTER"
echo "API_SERVICE=$API_SERVICE"
echo "SCHEDULER_SERVICE=$SCHEDULER_SERVICE"
echo "EVENTS_SERVICE=$EVENTS_SERVICE"
echo "TASK_DEF=$TASK_DEF"
echo "CONTAINER_NAME=$CONTAINER_NAME"
} >> "$GITHUB_ENV"
- name: Download ecs-tool
run: |
wget -O ecs-tool.tar.gz https://github.com/springload/ecs-tool/releases/download/1.9.6/ecs-tool_1.9.6_linux_amd64.tar.gz && tar -xvf ecs-tool.tar.gz ecs-tool
@@ -117,27 +65,14 @@ jobs:
- name: Deploy the images 🚀
run: |-
set -eu
# Run deploy.sh script before deployments
env "ECS_RUN.SERVICE=${API_SERVICE}" "ECS_RUN.LAUNCH_TYPE=FARGATE" \
./ecs-tool run -l "ecs-tool" \
--image_tag '{container_name}-${{ env.VERSION }}' \
--cluster "${CLUSTER}" \
--task_definition "${TASK_DEF}" \
--container_name "${CONTAINER_NAME}" \
./deploy.sh
# Deploy services
./ecs-tool deploy \
--image_tag '{container_name}-${{ env.VERSION }}' \
--cluster "${CLUSTER}" \
-s "${API_SERVICE}" -s "${SCHEDULER_SERVICE}" -s "${EVENTS_SERVICE}"
# run deploy.sh script before deployments
env "ECS_RUN.SERVICE=app" "ECS_RUN.LAUNCH_TYPE=FARGATE" ./ecs-tool run -l "ecs-tool" --image_tag '{container_name}-${{ env.VERSION }}' --cluster ${{ env.PROJECT }}-${{ env.STACK }} --task_definition ${{ env.PROJECT }}-${{ env.STACK }}-app --container_name app ./deploy.sh
./ecs-tool deploy --image_tag '{container_name}-${{ env.VERSION }}' --cluster ${{ env.PROJECT }}-${{ env.STACK }} -s app -s scheduler -s events
build_frontend:
name: Build and Deploy the Console
needs: [setup, build_service]
needs: [build_service]
runs-on: ubuntu-latest
environment: ${{ needs.setup.outputs.environment }}
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
@@ -208,20 +143,20 @@ jobs:
fi
working-directory: ./console
- name: Set Env Variables
- name: Set Env Variables for QA
if: startsWith(github.ref, 'refs/heads/deploy/qa')
run: |
# ensure file ends with a newline
printf '\n' >> ./environments/.env.production
echo "EXTENSIONS=${{ secrets.EXTENSIONS }}" >> ./environments/.env.production
echo "STRIPE_KEY=${{ secrets.STRIPE_TEST_KEY }}" >> ./environments/.env.production
echo "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
working-directory: ./console
- name: Set Env Variables for Production
if: startsWith(github.ref, 'refs/heads/deploy/production')
run: |
echo "STRIPE_KEY=${{ secrets.STRIPE_KEY }}" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_ANNUAL_ID=${{ secrets.STRIPE_PRICE_LICENSE_ANNUAL_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_MONTHLY_ID=${{ secrets.STRIPE_PRICE_LICENSE_MONTHLY_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_MAJOR_ID=${{ secrets.STRIPE_PRICE_LICENSE_MAJOR_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_MINOR_ID=${{ secrets.STRIPE_PRICE_LICENSE_MINOR_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_INSTALLATION_FULL_ID=${{ secrets.STRIPE_PRICE_INSTALLATION_FULL_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_INSTALLATION_ASSISTED_ID=${{ secrets.STRIPE_PRICE_INSTALLATION_ASSISTED_ID }}" >> ./environments/.env.production
echo "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
working-directory: ./console
- name: Install dependencies
@@ -240,11 +175,6 @@ jobs:
set -u
DEPLOY_BUCKET=${STATIC_DEPLOY_BUCKET:-${{ env.PROJECT }}-${{ env.STACK }}}
NEW_BUCKET="${PROJECT}-${STACK}-console"
if aws s3api head-bucket --bucket "$NEW_BUCKET" 2>/dev/null; then
DEPLOY_BUCKET="$NEW_BUCKET"
fi
# this value will come from the dotenv above
echo "Deploying to $DEPLOY_BUCKET"
wget -O- https://github.com/bep/s3deploy/releases/download/v2.11.0/s3deploy_2.11.0_linux-amd64.tar.gz | tar xzv -f - s3deploy

View File

@@ -1,21 +0,0 @@
name: Create Release
on:
push:
tags:
- 'v*'
jobs:
create:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body_path: RELEASE.md
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,61 +0,0 @@
name: Discord Announcement
on:
workflow_run:
workflows: ["Create Release"]
types: [completed]
workflow_dispatch:
inputs:
tag:
description: "Release tag to announce (e.g. v0.7.1)"
required: true
jobs:
discord_announcement:
runs-on: ubuntu-latest
steps:
# 1⃣ Figure out which tag were talking about
- id: vars
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${{ github.event.workflow_run.head_branch }}"
fi
echo "TAG=$TAG" >> "$GITHUB_ENV"
# 2⃣ Check out the exact commit for that tag
- uses: actions/checkout@v3
with:
ref: ${{ env.TAG }}
fetch-depth: 1
# 3⃣ Stash RELEASE.md in an env var (one atomic write → no EOF error)
- id: prep-body
shell: bash
run: |
body=$(<RELEASE.md)
max=4000
[[ ${#body} -gt $max ]] && body="${body:0:$max}…" # add ellipsis if trimmed
{
echo "body<<EOF"
echo "$body"
echo "EOF"
} >> "$GITHUB_OUTPUT"
# 4⃣ Fire the webhook
- uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
username: Fleetbase
content: |
@everyone
📦 **Fleetbase ${{ env.TAG }} released!**
<https://github.com/${{ github.repository }}/releases/tag/${{ env.TAG }}>
embed-title: "Fleetbase ${{ env.TAG }} — release notes"
embed-url: "https://github.com/fleetbase/fleetbase/releases/tag/${{ env.TAG }}"
embed-description: ${{ steps.prep-body.outputs.body }}
embed-color: 4362730 # 0x4291EA (Fleetbase Blue)

View File

@@ -0,0 +1,48 @@
name: Discord Announcement
on:
push:
tags:
- "v*"
jobs:
discord_announcement:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Get tag message
id: tag
run: |
echo "::set-output name=version::$(git describe --tags --abbrev=0)"
if [[ "${ACT}" == "true" ]]; then
# If running with act, use an environment variable for the tag message
echo "::set-output name=message::$(echo -e "${TAG_MESSAGE}" | base64)"
else
# If running on GitHub, use git to get the tag message
echo "::set-output name=message::$(git tag -l --format='%(contents)' $(git describe --tags --abbrev=0) | base64)"
fi
- name: Print tag message
run: echo "${{ steps.tag.outputs.message }}"
- name: Get tag name
id: get_tag
run: echo "::set-output name=tag::${GITHUB_REF/refs\/tags\//}"
- name: Decode message
id: decode
run: |
echo "Decoding message..."
echo "::set-output name=message::$(echo '${{ steps.tag.outputs.message }}' | base64 --decode)"
- name: Send message to Discord
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: "@everyone \n📦 New Fleetbase Version ${{ steps.get_tag.outputs.tag }} Released!\n${{ steps.decode.outputs.message }} \nVersion: ${{ steps.get_tag.outputs.tag }} \n[Release Notes for ${{ steps.get_tag.outputs.tag }}](https://github.com/fleetbase/fleetbase/releases/tag/${{ steps.get_tag.outputs.tag }})"
username: Fleetbase

View File

@@ -1,4 +1,4 @@
name: Fleetbase GCP CI/CD
name: Fleetbase CI/CD
on:
push:

View File

@@ -1,50 +0,0 @@
name: Fleetbase Docker Images
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
branch:
description: 'Branch to build from'
required: false
default: 'main'
version:
description: 'Image version tag (e.g., v0.7.1-beta)'
required: false
jobs:
docker-release:
name: Build and Push Docker Images
runs-on: ubuntu-latest
env:
REGISTRY: fleetbase
VERSION: ${{ github.event.inputs.version || (github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && github.ref_name) || 'manual' }}
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.branch || github.ref_name }}
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push Console & API Images
uses: docker/bake-action@v2
with:
push: true
targets: |
fleetbase-console
fleetbase-api
files: |
./docker-bake.hcl

25
.gitignore vendored
View File

@@ -3,11 +3,9 @@
.env.backup
.phpunit.result.cache
.pnpm-store
.tool-versions
docker-compose.override.yml
npm-debug.log
yarn-error.log
console/public/extensions.json
api/public/hot
api/public/storage
api/storage/*.key
@@ -18,8 +16,6 @@ api/composer.dev.json
api/composer-install-dev.sh
api/auth.json
api/crontab
api/go-crond
api/.fleetbase-id
act.sh
composer-auth.json
docker/database/*
@@ -35,24 +31,7 @@ packages/loconav
packages/internals
packages/projectargus-engine
packages/customer-portal
# wip
packages/solid
packages/aws-marketplace
packages/countries
packages/fliit
packages/samsara
packages/solid*
packages/valhalla
packages/vroom
solid
verdaccio
# asdf
.tools-versions
# binary build resources
builds/osx/frankenphp
# build artifacts
/builds/dist/
/builds/linux/spc/downloads/*
*.exe
*.dll
*.so
*.dylib
verdaccio

View File

@@ -8,7 +8,6 @@
http://:8000 {
root * /fleetbase/api/public
encode zstd br gzip
php_server {
resolve_root_symlink
}

510
README.md
View File

@@ -1,325 +1,185 @@
<div id="hero">
<p align="center" dir="auto">
<a href="https://fleetbase.io" rel="nofollow">
<img src="https://user-images.githubusercontent.com/58805033/191936702-fed04b0f-7966-4041-96d0-95e27bf98248.png" alt="Fleetbase logo" width="500" height="120" style="max-width: 100%;">
</a>
</p>
<p align="center" dir="auto">
<a href="https://github.com/fleetbase/fleetbase/blob/main/LICENSE"><img src="https://img.shields.io/github/license/fleetbase/fleetbase" alt="License"></a>
<a href="https://github.com/fleetbase/fleetbase/releases"><img src="https://img.shields.io/github/v/release/fleetbase/fleetbase" alt="Latest Release"></a>
<a href="https://github.com/fleetbase/fleetbase/stargazers"><img src="https://img.shields.io/github/stars/fleetbase/fleetbase?style=social" alt="GitHub Stars"></a>
<a href="https://discord.gg/V7RVWRQ2Wm"><img src="https://img.shields.io/discord/699834923032248430?logo=discord&label=Discord" alt="Discord"></a>
<a href="https://github.com/fleetbase/fleetbase/issues"><img src="https://img.shields.io/github/issues/fleetbase/fleetbase" alt="GitHub Issues"></a>
</p>
<p align="center" dir="auto">
Modular logistics and supply chain operating system
<br>
<a href="https://docs.fleetbase.io/" rel="nofollow" target="_fleetbase_docs">Documentation</a>
·
<a href="https://console.fleetbase.io" rel="nofollow" target="_fleetbase_console">Cloud Version</a>
·
<a href="https://tally.so/r/3NBpAW" rel="nofollow">Book a Demo</a>
·
<a href="https://discord.gg/V7RVWRQ2Wm" target="discord" rel="nofollow">Discord</a>
</p>
<hr />
</div>
## What is Fleetbase?
Fleetbase is a modular logistics and supply chain operating system designed to streamline management, planning, optimization, and operational control across various sectors of the supply chain industry.
<p align="center" dir="auto">
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleetbase_overview.png" alt="Fleetbase Console" width="1200" style="max-width: 100%;" />
</p>
## 🎯 Who Is Fleetbase For?
Fleetbase is designed for organizations that need powerful logistics and supply chain management:
- **E-commerce & Retail** - Manage deliveries, track orders, and optimize last-mile logistics
- **Food & Beverage** - Coordinate restaurant deliveries, manage drivers, and track real-time orders
- **Courier Services** - Dispatch drivers, optimize routes, and provide customer tracking
- **Field Services** - Schedule technicians, manage service areas, and track job completion
- **Enterprise Logistics** - Build custom supply chain solutions with full API access
- **Developers** - Extend and customize with a modular architecture and comprehensive API
## Visual Feature Showcase
| Feature | Screenshot | Description |
|---------|------------|-------------|
| **Order Board** | <img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-board-kanban.png" alt="Fleetbase Order Board" width="600" /> | Visualize and manage your orders with a dynamic Kanban board. |
| **Workflow Builder** | <img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-workflow-config.png" alt="Fleetbase Order Workflow Configuration" width="600" /> | Create custom order flows and automation with the intuitive workflow builder. |
| **Order Tracking** | <img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-map-view.png" alt="Fleetbase Order Map View" width="600" /> | Track individual orders in real-time on an interactive map. |
| **Live Fleet Map** | <img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/live-map-tracking.png" alt="Fleetbase Live Map Tracking" width="600" /> | Get a complete overview of your fleet and active orders on a live map. |
| **Service Zones** | <img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleet-map-zones.png" alt="Fleetbase Fleet Map with Zones" width="600" /> | Define and manage service areas and zones for your fleet. |
**Quickstart**
```bash
git clone git@github.com:fleetbase/fleetbase.git
cd fleetbase && ./scripts/docker-install.sh
```
## 📖 Table of contents
- [Features](#-features)
- [Install](#-install)
- [Extensions](#-extensions)
- [Apps](#-apps)
- [Roadmap](#-roadmap)
- [Deployment Options](#-deployment-options)
- [Bugs and Feature Requests](#-bugs-and--feature-requests)
- [Documentation](#-documentation)
- [Contributing](#-contributing)
- [Community](#-community)
- [Creators](#creators)
- [License & Copyright](#license--copyright)
## 📦 Features
| Feature | Description |
|---------|-------------|
| 🔌 **Extensible** | Build installable extensions and additional functionality directly into the OS via modular architecture. |
| 👨‍💻 **Developer Friendly** | RESTful API, socket, and webhooks to seamlessly integrate with external systems or develop custom applications. |
| 📱 **Native Apps** | Collection of open-source and native apps designed for operations and customer facing. |
| 🤝 **Collaboration** | Dedicated chat and comments system for collaboration across your organization. |
| 🔒 **Security** | Secure data encryption, adherence to industry-standard security practices, and a comprehensive dynamic Identity and Access Management (IAM) system. |
| 📡 **Telematics** | Integrate and connect to hardware devices and sensors to provide more feedback and visibility into operations. |
| 🌐 **Internationalized** | Translate into multiple languages to accommodate diverse user bases and global operations. |
| ⚙️ **Framework** | PHP core built around logistics and supply chain abstractions to streamline extension development. |
| 🔄 **Dynamic** | Configurable rules, flows and logic to enable automation and customization. |
| 🎨 **UI/UX** | Clean, responsive user-friendly interface for efficient management and operations from desktop or mobile. |
| 📊 **Dashboards** | Create custom dashboards and widgets to get full visibility into operations. |
| 📈 **Scalability** | Uninterrupted growth with scalable infrastructure and design, capable of handling increasing data volume and user demand as your business expands. |
| 🔄 **Continuous Improvements** | Commitment to continuous improvement, providing regular updates that seamlessly introduce optimizations, new features, and overall enhancements to the OS. |
| 🌍 **Open Source** | Deploy it either on-premise or in the cloud according to your organization's needs and preferences. |
## 💾 Install
Getting up and running with Fleetbase via Docker is the quickest and most straightforward way. If you'd like to use Fleetbase without docker read the [full install guide in the Fleetbase documentation](https://docs.fleetbase.io/getting-started/install).
Make sure you have both the latest versions of docker and docker-compose installed on your system.
```bash
git clone git@github.com:fleetbase/fleetbase.git
cd fleetbase && ./scripts/docker-install.sh
```
### Accessing Fleetbase
Once successfully installed and running you can then access the Fleetbase console on port 4200 and the API will be accessible from port 8000.
Fleetbase Console: http://localhost:4200
Fleetbase API: http://localhost:8000
### Additional Configurations
**CORS:** If you're installing directly on a server you will need to configure the environment variables to the application container:
```
CONSOLE_HOST=http://{yourhost}:4200
```
If you have additional applications or frontends you can use the environment variable `FRONTEND_HOSTS` to add a comma delimited list of additional frontend hosts.
**Application Key** If you get an issue about a missing application key just run:
```bash
docker compose exec application bash -c "php artisan key:generate --show"
```
Next copy this value to the `APP_KEY` environment variable in the application container and restart.
**Routing:** Fleetbase ships with a default OSRM server hosted by [router.project-osrm.org](https://router.project-osrm.org) but you're able to use your own or any other OSRM compatible server. You can modify this in the `console/environments` directory by modifying the .env file of the environment you're deploying and setting the `OSRM_HOST` to the OSRM server for Fleetbase to use.
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If you're deploying with docker then it's easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
```yaml
version: "3.8"
services:
application:
environment:
CONSOLE_HOST: http://localhost:4200
MAIL_MAILER: (ses, smtp, mailgun, postmark, sendgrid)
OSRM_HOST: https://router.project-osrm.org
IPINFO_API_KEY:
GOOGLE_MAPS_API_KEY:
GOOGLE_MAPS_LOCALE: us
TWILIO_SID:
TWILIO_TOKEN:
TWILIO_FROM:
socket:
environment:
# IMPORTANT: Configure WebSocket origins for security
# Development (localhost only - include WebSocket protocols):
SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*,https://localhost:*,ws://localhost:*,wss://localhost:*"}'
# Production (replace with your actual domain):
# SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*,wss://yourdomain.com:*"}'
```
**WebSocket Security:** The `SOCKETCLUSTER_OPTIONS` environment variable controls which domains can connect to the WebSocket server. Always restrict origins to your specific domains in production to prevent security vulnerabilities.
You can learn more about full installation, and configuration in the [official documentation](https://docs.fleetbase.io/getting-started/install).
# 🧩 Extensions
Extensions are modular components that enhance the functionality of your Fleetbase instance. They allow you to add new features, customize existing behavior, or integrate with external systems.
You can find extensions available from the official [Fleetbase Console](https://console.fleetbase.io), here you will also be able get your registry token to install extensions to a self-hosted Fleetbase instance.
Additionally you're able to develop and publish your own extensions as well which you can read more about developing extensions via the [extension building guide](https://docs.fleetbase.io/developers/building-an-extension).
## ⌨️ Fleetbase CLI
The Fleetbase CLI is a powerful tool designed to simplify the management of extensions for your Fleetbase instance. With the CLI, you can effortlessly handle authentication, install and uninstall extensions, and scaffold new extensions if you are developing your own.
Get started with the CLI with npm:
```bash
npm i -g @fleetbase/cli
```
Once installed, you can access a variety of commands to manage your Fleetbase extensions.
# 📱 Apps
Fleetbase offers open-source mobile apps that can be customized and deployed:
| App | Description | Platform | Repository |
|-----|-------------|----------|------------|
| **Storefront App** | E-commerce/on-demand app for launching your own shop or marketplace | iOS & Android | [GitHub](https://github.com/fleetbase/storefront-app) |
| **Navigator App** | Driver app for managing orders with real-time location tracking | iOS & Android | [GitHub](https://github.com/fleetbase/navigator-app) |
## 🛣️ Roadmap
| Feature | Status | Expected Release | Description |
|---------|--------|------------------|-------------|
| **Pallet (WMS)** | 🚧 In Development | Late Q1 / Early Q2 2026 | Inventory and Warehouse Management extension |
| **Ledger** | 🚧 In Development | Late Q1 / Early Q2 2026 | Accounting and Invoicing extension |
| **AI Agent** | 🔬 Research | Q4 2026 | AI integration for system and workflow automation |
| **Dynamic Rules** | 📋 Planned | 2027 | Rule builder to trigger events, tasks, and jobs |
Want to influence our roadmap? [Join the discussion](https://github.com/orgs/fleetbase/discussions)
## 🚀 Deployment Options
| Option | Best For | Setup Time | Maintenance |
|--------|----------|------------|-------------|
| **Docker (Local)** | Development & Testing | 5 minutes | Self-managed |
| **On-Premise** | Production on your own infrastructure | 30-60 minutes | Self-managed |
| **Cloud Self-Hosted** | Production (AWS, GCP, Azure) | 30-60 minutes | Self-managed |
| **Fleetbase Cloud** | Quick start, no DevOps | Instant | Fully managed |
[View detailed deployment guides →](https://docs.fleetbase.io/category/deploying)
## 🐛 Bugs and 💡 Feature Requests
Have a bug or a feature request? Please check the <a href="https://github.com/fleetbase/fleetbase/issues">issue tracker</a> and search for existing and closed issues. If your problem or idea is not addressed yet, please <a href="https://github.com/fleetbase/fleetbase/issues/new">open a new issue</a>.
## 📄 Documentation
Fleetbase has comprehensive documentation to help you get started and make the most of the platform:
- **Getting Started**: [Installation Guide](https://docs.fleetbase.io/getting-started/install)
- **API Reference**: [API Documentation](https://docs.fleetbase.io/api-reference)
- **Extension Development**: [Building Extensions](https://docs.fleetbase.io/developers/building-an-extension)
- **Deployment**: [Deployment Guides](https://docs.fleetbase.io/deployment)
## 🤝 Contributing
We welcome contributions from the community! Here's how you can help:
- **Report Bugs**: [Open an issue](https://github.com/fleetbase/fleetbase/issues/new)
- **Suggest Features**: [Start a discussion](https://github.com/orgs/fleetbase/discussions)
- **Submit PRs**: Read our [Contributing Guide](https://github.com/fleetbase/fleetbase/blob/main/CONTRIBUTING.md)
- **Write Documentation**: Help improve our [docs](https://docs.fleetbase.io)
- **Build Extensions**: Create and share [extensions](https://docs.fleetbase.io/developers/building-an-extension)
**Development Setup**: See our [Development Installation Guide](https://docs.fleetbase.io/getting-started/install/for-development) for detailed instructions on setting up your local development environment.
## 👥 Community
Get updates on Fleetbase's development and chat with the project maintainers and community members by joining our <a href="https://discord.gg/V7RVWRQ2Wm">Discord</a>.
<ul>
<li>Follow <a href="https://x.com/fleetbase_io">@fleetbase_io on X</a>.</li>
<li>Read and subscribe to <a href="https://www.fleetbase.io/blog-2">The Official Fleetbase Blog</a>.</li>
<li>Ask and explore <a href="https://github.com/orgs/fleetbase/discussions">our GitHub Discussions</a>.</li>
</ul>
<p dir="auto">See the <a href="https://github.com/fleetbase/fleetbase/releases">Releases</a> section of our GitHub project for changelogs for each release version of Fleetbase.</p>
<p>Release announcement posts on <a href="https://www.fleetbase.io/blog-2" rel="nofollow">the official Fleetbase blog</a> contain summaries of the most noteworthy changes made in each release.</p>
## Creators
<table style="border: none;">
<tr>
<td align="center" style="border: none;">
<img src="https://user-images.githubusercontent.com/58805033/230263021-212f2553-1269-473d-be94-313cb3eecfa5.png" alt="Ronald A. Richardson" width="120" height="120" style="border-radius: 50%;">
<br>
<strong>Ronald A. Richardson</strong>
<br>
Co-founder & CTO
<br>
<a href="https://github.com/orgs/fleetbase/people/roncodes">GitHub</a> | <a href="https://www.linkedin.com/in/ronald-a-richardson/">LinkedIn</a>
</td>
<td align="center" style="border: none;">
<img src="https://user-images.githubusercontent.com/58805033/230262598-1ce6d0cc-fb65-41f9-8384-5cf5cbf369c7.png" alt="Shiv Thakker" width="120" height="120" style="border-radius: 50%;">
<br>
<strong>Shiv Thakker</strong>
<br>
Co-founder & CEO
<br>
<a href="https://github.com/orgs/fleetbase/people/shivthakker">GitHub</a> | <a href="https://www.linkedin.com/in/shivthakker/">LinkedIn</a>
</td>
</tr>
</table>
# License & Copyright
Fleetbase is available under a **dual-licensing model** to accommodate both open-source community users and commercial enterprises:
## Open Source License (AGPL-3.0)
By default, Fleetbase is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html). This license allows you to use, modify, and distribute Fleetbase freely, provided that:
- Any modifications or derivative works are also made available under AGPL-3.0
- If you run a modified version as a network service, you must make the source code available to users
The AGPL-3.0 is ideal for open-source projects, academic research, and organizations committed to sharing their improvements with the community.
## Commercial License (FCL)
For organizations that require more flexibility, Fleetbase offers a **Fleetbase Commercial License (FCL)** that provides:
- **Freedom from AGPL obligations** Deploy and modify Fleetbase without source code disclosure requirements
- **Proprietary integrations** Build closed-source extensions and integrations
- **Commercial protections** Warranties, indemnities, and legal assurances not provided under AGPL
- **Derivative work ownership** Retain full ownership of your modifications and customizations
- **Flexible licensing options** Choose from annual, monthly, or perpetual license models
### Commercial License Options
| License Type | Price | Support & Updates | Best For |
|--------------|-------|-------------------|----------|
| **Annual License** | $25,000/year | ✅ All upgrades & Business Support included | Organizations requiring continuous updates and support |
| **Monthly License** | $2,500/month | ✅ All upgrades & Business Support included | Pilot projects and short-term deployments |
| **Major Version License** | $25,000 (one-time) | ❌ No ongoing support | Stable deployments on a single major version |
| **Minor Version License** | $15,000 (one-time) | ❌ No ongoing support | Locked version deployments |
### When You Need a Commercial License
You should consider a commercial license if you:
- Want to build proprietary extensions or integrations without open-sourcing them
- Need to embed Fleetbase in a commercial product without AGPL obligations
- Require enterprise-grade support, SLAs, and legal protections
- Plan to modify Fleetbase without sharing your changes publicly
### Get a Commercial License
For more information about commercial licensing options, please contact us:
- **Email:** [hello@fleetbase.io](mailto:hello@fleetbase.io)
- **Website:** [fleetbase.io](https://fleetbase.io)
---
**Copyright © 2026 Fleetbase Pte. Ltd.** All rights reserved.
<div id="hero">
<p align="center" dir="auto">
<a href="https://fleetbase.io" rel="nofollow">
<img src="https://user-images.githubusercontent.com/58805033/191936702-fed04b0f-7966-4041-96d0-95e27bf98248.png" alt="Fleetbase logo" width="500" height="120" style="max-width: 100%;">
</a>
</p>
<p align="center" dir="auto">
Modular logistics and supply chain operating system
<br>
<a href="https://docs.fleetbase.io/" rel="nofollow">Documentation</a>
·
<a href="https://console.fleetbase.io" rel="nofollow">Cloud Version</a>
·
<a href="https://fleetbase.apichecker.com" target="_api_status" rel="nofollow">API Status</a>
·
<a href="https://tally.so/r/3NBpAW" rel="nofollow">Book a Demo</a>
·
<a href="https://discord.gg/V7RVWRQ2Wm" target="discord" rel="nofollow">Discord</a>
</p>
<hr />
</div>
## What is Fleetbase?
Fleetbase is a modular logistics and supply chain operating system designed to streamline management, planning, optimization, and operational control across various sectors of the supply chain industry.
<p align="center" dir="auto">
<img src="https://github.com/fleetbase/fleetbase/assets/816371/125348c9-c88a-49fe-b098-9abec9d7dff8" alt="Fleetbase Console" width="1200" style="max-width: 100%;" />
</p>
**Quickstart**
```bash
git clone git@github.com:fleetbase/fleetbase.git
cd fleetbase
docker-compose up -d
docker exec -ti fleetbase-application-1 bash
sh deploy.sh
```
## 📖 Table of contents
- [Features](#-features)
- [Install](#-install)
- [Extensions](#-extensions)
- [Apps](#-apps)
- [Roadmap](#-roadmap)
- [Bugs and Feature Requests](#-bugs-and--feature-requests)
- [Documentation](#-documentation)
- [Contributing](#-contributing)
- [Community](#-community)
- [Creators](#-creators)
- [License & Copyright](#-license-and-copyright)
## 📦 Features
- **Extensible:** Build installable extensions and additional functionality directly into the OS via modular architecture.
- **Developer Friendly:** RESTful API, socket, and webhooks to seamlessly integrate with external systems or develop custom applications.
- **Native Apps:** Collection of open-source and native apps designed for operations and customer facing.
- **Collaboration:** Dedicated chat and comments system for collaboration across your organization.
- **Security:** Secure data encryption, adherence to industry-standard security practices, and a comprehensive dynamic Identity and Access Management (IAM) system.
- **Telematics:** Integrate and connect to hardware devices and sensors to provide more feedback and visibility into operations.
- **Internationalized:** Translate into multiple languages to accommodate diverse user bases and global operations.
- **Framework:** PHP core built around logistics and supply chain abstractions to streamline extension development.
- **Dynamic:** Configurable rules, flows and logic to enable automation and customization.
- **UI/UX:** Clean, responsive user-friendly interface for efficient management and operations from desktop or mobile.
- **Dashboards:** Create custom dashboards and widgets to get full visibility into operations.
- **Scalability:** Uninterrupted growth with scalable infrastructure and design, capable of handling increasing data volume and user demand as your business expands.
- **Continuous Improvements:** Commitment to continuous improvement, providing regular updates that seamlessly introduce optimizations, new features, and overall enhancements to the OS.
- **Open Source:** Deploy it either on-premise or in the cloud according to your organization's needs and preferences.
## 💾 Install
Getting up and running with Fleetbase via Docker is the quickest and most straightforward way. If youd like to use Fleetbase without docker read the [full install guide in the Fleetbase documentation](https://docs.fleetbase.io/getting-started/install).
Make sure you have both the latest versions of docker and docker-compose installed on your system.
```bash
git clone git@github.com:fleetbase/fleetbase.git
cd fleetbase
docker-compose up -d
docker exec -ti fleetbase-application-1 bash
sh deploy.sh
```
### Accessing Fleetbase
Once successfully installed and running you can then access the Fleetbase console on port 4200 and the API will be accessible from port 8000.
Fleetbase Console: http://localhost:4200
Fleetbase API: http://localhost:8000
### Additional Configurations
**CORS:** If youre installing directly on a server you may need to add your IP address or domain to the `api/config/cors.php` file in the `allowed_hosts` array.
**Routing:** Fleetbase ships with a default OSRM server hosted by `[router.project-osrm.org](https://router.project-osrm.org)` but youre able to use your own or any other OSRM compatible server. You can modify this in the `console/environments` directory by modifying the .env file of the environment youre deploying and setting the `OSRM_HOST` to the OSRM server for Fleetbase to use.
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If youre deploying with docker then its easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
```yaml
version: “3.8”
services:
application:
environment:
MAIL_MAILER: (ses, smtp, mailgun, postmark, sendgrid)
OSRM_HOST: https://router.project-osrm.org
IPINFO_API_KEY:
GOOGLE_MAPS_API_KEY:
GOOGLE_MAPS_LOCALE: us
TWILIO_SID:
TWILIO_TOKEN:
TWILIO_FROM:
CONSOLE_HOST: http://localhost:4200
```
You can learn more about full installation, and configuration in the [official documentation](https://docs.fleetbase.io/getting-started/install).
# 🧩 Extensions
Extensions are modular components that enhance the functionality of your Fleetbase instance. They allow you to add new features, customize existing behavior, or integrate with external systems.
You can find extensions available from the official [Fleetbase Console](https://console.fleetbase.io), here you will also be able get your registry token to install extensions to a self-hosted Fleetbase instance.
Additionally you're able to develop and publish your own extensions as well which you can read more about developing extensions via the [extension building guide](https://docs.fleetbase.io/developers/building-an-extension).
## ⌨️ Fleetbase CLI
The Fleetbase CLI is a powerful tool designed to simplify the management of extensions for your Fleetbase instance. With the CLI, you can effortlessly handle authentication, install and uninstall extensions, and scaffold new extensions if you are developing your own.
Get started with the CLI with npm:
```bash
npm i -g @fleetbase/cli
```
Once installed, you can access a variety of commands to manage your Fleetbase extensions.
# 📱 Apps
Fleetbase offers a few open sourced apps which are built on Fleetbase which can be cloned and customized. Every app is built so that the Fleetbase instance can be switched out whether on-premise install or cloud hosted.
<ul>
<li><a href="https://github.com/fleetbase/storefront-app">Storefront App</a>: Fleetbase based ecommerce/on-demand app for launching your very own shop or marketplace to Apple or Android playstore.</li>
<li><a href="https://github.com/fleetbase/navigator-app">Navigator App</a>: Fleetbase based driver app which can be used for drivers to manage and update order, additionally provides real time driver location which can be viewed in the Fleetbase Console.</li>
</ul>
## 🛣️ Roadmap
1. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
2. **Accounting and Invoicing** ~ Ledger will be Fleetbases first official extension accounting and invoicing.
3. **Binary Builds** ~ Run Fleetbase from a single binary.
4. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
5. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
## 🪲 Bugs and 💡 Feature Requests
Have a bug or a feature request? Please check the <a href="https://github.com/fleetbase/fleetbase/issues">issue tracker</a> and search for existing and closed issues. If your problem or idea is not addressed yet, please <a href="https://github.com/fleetbase/fleetbase/issues/new">open a new issue</a>.
## 👨‍💻 Contributing
Please read through our <a href="https://github.com/fleetbase/fleetbase/blob/main/CONTRIBUTING.md">contributing guidelines</a>. Included are directions for opening issues, coding standards, and notes on development.
## 👥 Community
Get updates on Fleetbase's development and chat with the project maintainers and community members by joining our <a href="https://discord.gg/V7RVWRQ2Wm">Discord</a>.
<ul>
<li>Follow <a href="https://x.com/fleetbase_io">@fleetbase_io on X</a>.</li>
<li>Read and subscribe to <a href="https://www.fleetbase.io/blog-2">The Official Fleetbase Blog</a>.</li>
<li>Ask and explore <a href="https://github.com/orgs/fleetbase/discussions">our GitHub Discussions</a>.</li>
</ul>
<p dir="auto">See the <a href="https://github.com/fleetbase/fleetbase/releases">Releases</a> section of our GitHub project for changelogs for each release version of Fleetbase.</p>
<p>Release announcement posts on <a href="https://www.fleetbase.io/blog-2" rel="nofollow">the official Fleetbase blog</a> contain summaries of the most noteworthy changes made in each release.</p>
## Creators
<p dir="auto"><strong>Ronald A. Richardson</strong>- Co-founder &amp; CTO</p>
<img src="https://user-images.githubusercontent.com/58805033/230263021-212f2553-1269-473d-be94-313cb3eecfa5.png" alt="Ron Image" width="75" height="75" style="max-width: 100%;">
<p><a href="https://github.com/orgs/fleetbase/people/roncodes">Github</a> | <a href="https://www.linkedin.com/in/ronald-a-richardson/">LinkedIn</a></p>
<p dir="auto"><strong>Shiv Thakker</strong> - Co-founder &amp; CEO</p>
<img src="https://user-images.githubusercontent.com/58805033/230262598-1ce6d0cc-fb65-41f9-8384-5cf5cbf369c7.png" alt="Shiv Image" width="75" height="75" style="max-width: 100%;">
<p><a href="https://github.com/orgs/fleetbase/people/shivthakker">Github</a> | <a href="https://www.linkedin.com/in/shivthakker/">LinkedIn</a></p>
# License & Copyright
Fleetbase is made available under the terms of the <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">GNU Affero General Public License 3.0 (AGPL 3.0)</a>. For other licenses <a href="mailto:hello@fleetbase.io" target="_blank">contact us</a>.

View File

@@ -1,34 +0,0 @@
# 🚀 Fleetbase v0.7.28 — 2026-02-05
> "Critical patch: eager load driver and vehicle in consumable orders query API"
---
## ✨ Highlights
- Critical patch which fixes eager loading relationship when querying orders via the consumable API, this is done to handle the new `whenLoaded` resource conditional methods. [fleetbase/fleetops#203](https://github.com/fleetbase/fleetops/pull/203)
---
## ⚠️ Breaking Changes
- None 🙂
---
## 🔧 Upgrade Steps
```bash
# Pull latest version
git pull origin main --no-rebase
# Update docker
docker compose pull
docker compose down && docker compose up -d
# Run deploy script
docker compose exec application bash -c "./deploy.sh"
```
---
## Need help?
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/ember-ui/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)

View File

@@ -1,100 +0,0 @@
# Contributing to Fleetbase Translations
First off, thank you for considering contributing to Fleetbase translations! Your efforts help make Fleetbase accessible to a global audience. This guide will walk you through the process of adding or updating language translations for the Fleetbase platform and its various extensions.
## Understanding the Structure
Fleetbase is a modular system. The main application, known as Fleetbase Console, has its own set of translations. Additionally, each extension (like FleetOps or Storefront) also contains its own translation files. This means that to provide a complete translation for a specific language, you may need to contribute to multiple repositories.
- **Main Application (`fleetbase/fleetbase`)**: Contains the core translation files for the Fleetbase Console.
- **Extensions/Modules**: Each extension has its own repository and its own set of translation files.
## File Format and Location
All translation files are in the **YAML** format (`.yaml` or `.yml`). The base language for all translations is American English (`en-us.yaml`).
- In the main `fleetbase/fleetbase` repository, the translation files are located at `./console/translations/`.
- In each extension repository, the translation files are located at `./translations/`.
Translation files are named using the language and region code, for example:
- `en-us.yaml` (American English)
- `fr-fr.yaml` (French, France)
- `zh-cn.yaml` (Chinese, Simplified)
## How to Contribute Translations
Follow these steps to contribute a new translation or update an existing one.
### Step 1: Fork and Clone the Repository
First, you need to fork the repository you want to contribute to. This could be the main `fleetbase/fleetbase` repository or one of the extension repositories. After forking, clone it to your local machine.
### Step 2: Create or Update a Language File
Navigate to the appropriate translations directory (`./console/translations/` or `./translations/`).
- **To add a new language**: Copy the `en-us.yaml` file and rename it to your target language code (e.g., `es-es.yaml`).
- **To update an existing language**: Open the existing language file. You can compare it with `en-us.yaml` to find missing keys or phrases that need updating.
### Step 3: Translate the Content
Open the YAML file in a text editor. You will see a structure of nested keys and values.
```yaml
# Example from en-us.yaml
common:
new: New
create: Create
delete-selected-count: Delete {count} Selected
```
When translating, you should:
- **Only translate the values**, not the keys. For example, in `new: New`, you would only translate `New`.
- **Keep placeholders intact**. Some phrases contain placeholders like `{count}` or `{resource}`. These should not be translated. They are used by the application to insert dynamic values.
Here is an example of the French translation for the keys above:
```yaml
# Example from fr-fr.yaml
common:
new: Nouveau
create: Créer
delete-selected-count: Supprimer {count} sélectionné(s)
```
### Step 4: Submit a Pull Request
Once you have finished translating, commit your changes and push them to your forked repository. Then, open a pull request to the original Fleetbase repository.
- Make sure your pull request has a clear title and description of the changes you made.
- If you are translating an extension, you may need to submit a pull request to the extension's repository. If your changes also affect the main console, a separate PR to the `fleetbase/fleetbase` repository might be necessary.
Your contribution will be reviewed by the Fleetbase team, and once approved, it will be merged into the project.
## Translation Repositories
Here is a list of the primary repositories that accept translation contributions:
| Repository | Translation Path |
| ---------------------------------------- | ----------------------------- |
| [fleetbase/fleetbase][1] | `./console/translations/` |
| [fleetbase/fleetops][2] | `./translations/` |
| [fleetbase/storefront][3] | `./translations/` |
| [fleetbase/dev-engine][4] | `./translations/` |
| [fleetbase/iam-engine][5] | `./translations/` |
| [fleetbase/pallet][6] | `./translations/` |
| [fleetbase/ledger][7] | `./translations/` |
| [fleetbase/registry-bridge][8] | `./translations/` |
[1]: https://github.com/fleetbase/fleetbase
[2]: https://github.com/fleetbase/fleetops
[3]: https://github.com/fleetbase/storefront
[4]: https://github.com/fleetbase/dev-engine
[5]: https://github.com/fleetbase/iam-engine
[6]: https://github.com/fleetbase/pallet
[7]: https://github.com/fleetbase/ledger
[8]: https://github.com/fleetbase/registry-bridge
Thank you again for your contribution to the Fleetbase community!

View File

@@ -40,6 +40,8 @@ class Kernel extends HttpKernel
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

View File

@@ -2,8 +2,10 @@
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
@@ -15,15 +17,17 @@ class RouteServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(
function () {
Route::get(
'/health',
function (Request $request) {
'/status',
function () {
return response()->json(
[
'status' => 'ok',
'time' => microtime(true) - $request->attributes->get('request_start_time')
'time' => microtime(true) - LARAVEL_START
]
);
}
@@ -31,4 +35,19 @@ class RouteServiceProvider extends ServiceProvider
}
);
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for(
'api',
function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
}
);
}
}

View File

@@ -7,42 +7,31 @@
"laravel"
],
"license": "AGPL-3.0-or-later",
"authors": [
{
"name": "Fleetbase Pte Ltd.",
"email": "hello@fleetbase.io"
},
{
"name": "Ronald A. Richardson",
"email": "ron@fleetbase.io"
}
],
"require": {
"php": ">=8.0 <=8.2.30",
"php": "^8.0",
"appstract/laravel-opcache": "^4.0",
"fleetbase/core-api": "^1.6.35",
"fleetbase/fleetops-api": "^0.6.35",
"fleetbase/registry-bridge": "^0.1.5",
"fleetbase/storefront-api": "^0.4.13",
"fleetbase/core-api": "^1.6.2",
"fleetbase/fleetops-api": "^0.6.5",
"fleetbase/registry-bridge": "^0.0.18",
"fleetbase/storefront-api": "^0.3.30",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",
"laravel/tinker": "^2.9",
"league/flysystem-aws-s3-v3": "^3.0",
"maatwebsite/excel": "^3.1",
"maennchen/zipstream-php": "3.1.2",
"phpoffice/phpspreadsheet": "^1.28",
"predis/predis": "^2.1",
"psr/http-factory-implementation": "*",
"resend/resend-php": "^0.14.0",
"s-ichikawa/laravel-sendgrid-driver": "^4.0",
"stripe/stripe-php": "^17.0",
"symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.1"
},
"require-dev": {
"spatie/laravel-ignition": "^2.0",
"fakerphp/faker": "^1.9.1",
"kitloong/laravel-migrations-generator": "^6.10",
"laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
@@ -89,6 +78,15 @@
],
"clean-logs": [
"composer run-script clear-logs"
],
"dock": [
"docker exec -it fleetbase_os_application_1 /usr/bin/tmux -u new"
],
"dock-server": [
"docker exec -it fleetbase_os_httpd_1 /bin/sh"
],
"tunnel": [
"ngrok http --region=ap --hostname=fleetbase.ap.ngrok.io 8000"
]
},
"extra": {

3730
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ return [
|
*/
'env' => env('APP_ENV', env('ENVIRONMENT', 'production')),
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------

View File

@@ -17,17 +17,17 @@ return [
|
*/
'paths' => ['*', 'sanctum/csrf-cookie'],
'paths' => ['/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST')), ...Utils::arrayFrom(env('FRONTEND_HOSTS', ''))]),
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST'))]),
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key', 'content-disposition'],
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key'],
'max_age' => 0,

View File

@@ -51,7 +51,7 @@ return [
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'stdout'],
'channels' => ['single'],
'ignore_exceptions' => false,
],

View File

@@ -66,18 +66,6 @@ return [
'resend' => [],
'microsoft-graph' => [
'transport' => 'microsoft-graph',
'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),
'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),
'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@fleetbase.io'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Fleetbase')),
],
'save_to_sent_items' => env('MAIL_SAVE_TO_SENT_ITEMS', false),
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),

View File

@@ -1,6 +1,5 @@
<?php
use Fleetbase\Support\Utils;
use Laravel\Octane\Contracts\OperationTerminated;
use Laravel\Octane\Events\RequestHandled;
use Laravel\Octane\Events\RequestReceived;
@@ -105,8 +104,8 @@ return [
OperationTerminated::class => [
FlushOnce::class,
FlushTemporaryContainerInstances::class,
DisconnectFromDatabases::class,
CollectGarbage::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
],
WorkerErrorOccurred::class => [
@@ -193,7 +192,6 @@ return [
'routes',
'composer.lock',
'.env',
...Utils::arrayFrom(env('OCTANE_WATCH_DIRS'))
],
/*

View File

@@ -43,10 +43,4 @@ return [
'secret' => env('STRIPE_SECRET', env('STRIPE_API_SECRET')),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
'callpromn' => [
'api_key' => env('CALLPROMN_API_KEY', ''),
'from' => env('CALLPROMN_FROM', ''),
'base_url' => env('CALLPROMN_BASE_URL', 'https://api.messagepro.mn' ),
],
];

View File

@@ -1,19 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة المصادقة
|--------------------------------------------------------------------------
|
| تحتوي الأسطر التالية على رسائل المصادقة التي نعرضها للمستخدم أثناء
| عمليات تسجيل الدخول أو غيرها. يمكنك تعديل هذه الرسائل حسب متطلباتك.
|
*/
'failed' => 'بيانات الاعتماد هذه غير متطابقة مع سجلاتنا.',
'password' => 'كلمة المرور التي تم إدخالها غير صحيحة.',
'throttle' => 'عدد كبير جداً من محاولات الدخول. يرجى المحاولة مرة أخرى خلال :seconds ثانية.',
];

View File

@@ -1,19 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة الترقيم الصفحي
|--------------------------------------------------------------------------
|
| تُستخدم الأسطر التالية من قبل مكتبة الترقيم الصفحي لبناء روابط
| الترقيم البسيطة. يمكنك تعديلها كما تشاء لتخصيص العرض بما يناسب
| تطبيقك بشكل أفضل.
|
*/
'previous' => '&laquo; السابق',
'next' => 'التالي &raquo;',
];

View File

@@ -1,21 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة إعادة تعيين كلمة المرور
|--------------------------------------------------------------------------
|
| الأسطر التالية هي الرسائل الافتراضية التي يقدمها نظام إعادة تعيين
| كلمة المرور عند فشل المحاولة، مثل رمز التحقق غير صالح أو كلمة مرور جديدة غير صحيحة.
|
*/
'reset' => 'تم إعادة تعيين كلمة المرور الخاصة بك!',
'sent' => 'لقد أرسلنا رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني!',
'throttled' => 'يرجى الانتظار قبل المحاولة مرة أخرى.',
'token' => 'رمز إعادة تعيين كلمة المرور هذا غير صالح.',
'user' => 'لا يمكننا العثور على مستخدم بهذا العنوان الإلكتروني.',
];

View File

@@ -1,168 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'يجب قبول :attribute.',
'accepted_if' => 'يجب قبول :attribute عندما يكون :other يساوي :value.',
'active_url' => ':attribute ليس عنوان URL صالحًا.',
'after' => 'يجب أن يكون :attribute تاريخًا بعد :date.',
'after_or_equal' => 'يجب أن يكون :attribute تاريخًا بعد أو يساوي :date.',
'alpha' => 'يجب أن يحتوي :attribute على أحرف فقط.',
'alpha_dash' => 'يجب أن يحتوي :attribute على أحرف وأرقام وشرطات فقط.',
'alpha_num' => 'يجب أن يحتوي :attribute على أحرف وأرقام فقط.',
'array' => 'يجب أن يكون :attribute مصفوفة.',
'before' => 'يجب أن يكون :attribute تاريخًا قبل :date.',
'before_or_equal' => 'يجب أن يكون :attribute تاريخًا قبل أو يساوي :date.',
'between' => [
'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
'file' => 'يجب أن يكون :attribute بين :min و :max كيلوبايت.',
'string' => 'يجب أن يكون :attribute بين :min و :max حرفًا.',
'array' => 'يجب أن يحتوي :attribute على عدد عناصر بين :min و :max.',
],
'boolean' => 'يجب أن يكون حقل :attribute صحيحًا أو خاطئًا.',
'confirmed' => 'تأكيد :attribute غير متطابق.',
'current_password' => 'كلمة المرور غير صحيحة.',
'date' => ':attribute ليس تاريخًا صالحًا.',
'date_equals' => 'يجب أن يكون :attribute تاريخًا يساوي :date.',
'date_format' => 'لا يتطابق :attribute مع الصيغة :format.',
'declined' => 'يجب رفض :attribute.',
'declined_if' => 'يجب رفض :attribute عندما يكون :other يساوي :value.',
'different' => 'يجب أن يكون :attribute و :other مختلفين.',
'digits' => 'يجب أن يتكون :attribute من :digits أرقام.',
'digits_between' => 'يجب أن يتكون :attribute من :min إلى :max أرقام.',
'dimensions' => ':attribute يحتوي على أبعاد صورة غير صالحة.',
'distinct' => 'حقل :attribute يحتوي على قيمة مكررة.',
'email' => 'يجب أن يكون :attribute عنوان بريد إلكتروني صالحًا.',
'ends_with' => 'يجب أن ينتهي :attribute بأحد القيم التالية: :values.',
'enum' => ':attribute المحدد غير صالح.',
'exists' => ':attribute المحدد غير صالح.',
'file' => 'يجب أن يكون :attribute ملفًا.',
'filled' => 'يجب أن يحتوي حقل :attribute على قيمة.',
'gt' => [
'numeric' => 'يجب أن يكون :attribute أكبر من :value.',
'file' => 'يجب أن يكون :attribute أكبر من :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أكبر من :value حرفًا.',
'array' => 'يجب أن يحتوي :attribute على أكثر من :value عنصر.',
],
'gte' => [
'numeric' => 'يجب أن يكون :attribute أكبر من أو يساوي :value.',
'file' => 'يجب أن يكون :attribute أكبر من أو يساوي :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أكبر من أو يساوي :value حرفًا.',
'array' => 'يجب أن يحتوي :attribute على :value عنصر أو أكثر.',
],
'image' => 'يجب أن يكون :attribute صورة.',
'in' => ':attribute المحدد غير صالح.',
'in_array' => 'حقل :attribute غير موجود في :other.',
'integer' => 'يجب أن يكون :attribute عددًا صحيحًا.',
'ip' => 'يجب أن يكون :attribute عنوان IP صالحًا.',
'ipv4' => 'يجب أن يكون :attribute عنوان IPv4 صالحًا.',
'ipv6' => 'يجب أن يكون :attribute عنوان IPv6 صالحًا.',
'json' => 'يجب أن يكون :attribute نصًا بصيغة JSON صالحة.',
'lt' => [
'numeric' => 'يجب أن يكون :attribute أقل من :value.',
'file' => 'يجب أن يكون :attribute أقل من :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أقل من :value حرفًا.',
'array' => 'يجب أن يحتوي :attribute على أقل من :value عنصر.',
],
'lte' => [
'numeric' => 'يجب أن يكون :attribute أقل من أو يساوي :value.',
'file' => 'يجب أن يكون :attribute أقل من أو يساوي :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أقل من أو يساوي :value حرفًا.',
'array' => 'يجب ألا يحتوي :attribute على أكثر من :value عنصر.',
],
'mac_address' => 'يجب أن يكون :attribute عنوان MAC صالحًا.',
'max' => [
'numeric' => 'يجب ألا يتجاوز :attribute :max.',
'file' => 'يجب ألا يتجاوز :attribute :max كيلوبايت.',
'string' => 'يجب ألا يتجاوز :attribute :max حرفًا.',
'array' => 'يجب ألا يحتوي :attribute على أكثر من :max عنصر.',
],
'mimes' => 'يجب أن يكون :attribute ملفًا من النوع: :values.',
'mimetypes' => 'يجب أن يكون :attribute ملفًا من النوع: :values.',
'min' => [
'numeric' => 'يجب أن يكون :attribute على الأقل :min.',
'file' => 'يجب أن يكون :attribute على الأقل :min كيلوبايت.',
'string' => 'يجب أن يكون :attribute على الأقل :min حرفًا.',
'array' => 'يجب أن يحتوي :attribute على الأقل على :min عنصر.',
],
'multiple_of' => 'يجب أن يكون :attribute مضاعفًا لـ :value.',
'not_in' => ':attribute المحدد غير صالح.',
'not_regex' => 'صيغة :attribute غير صالحة.',
'numeric' => 'يجب أن يكون :attribute رقمًا.',
'password' => 'كلمة المرور غير صحيحة.',
'present' => 'يجب أن يكون حقل :attribute موجودًا.',
'prohibited' => 'حقل :attribute محظور.',
'prohibited_if' => 'حقل :attribute محظور عندما يكون :other يساوي :value.',
'prohibited_unless' => 'حقل :attribute محظور إلا إذا كان :other ضمن :values.',
'prohibits' => 'حقل :attribute يحظر وجود :other.',
'regex' => 'صيغة :attribute غير صالحة.',
'required' => 'حقل :attribute مطلوب.',
'required_array_keys' => 'يجب أن يحتوي حقل :attribute على إدخالات لـ: :values.',
'required_if' => 'حقل :attribute مطلوب عندما يكون :other يساوي :value.',
'required_unless' => 'حقل :attribute مطلوب إلا إذا كان :other ضمن :values.',
'required_with' => 'حقل :attribute مطلوب عند وجود :values.',
'required_with_all' => 'حقل :attribute مطلوب عند وجود جميع القيم :values.',
'required_without' => 'حقل :attribute مطلوب عند عدم وجود :values.',
'required_without_all' => 'حقل :attribute مطلوب عند عدم وجود أي من القيم :values.',
'same' => 'يجب أن يتطابق :attribute مع :other.',
'size' => [
'numeric' => 'يجب أن يكون :attribute مساويًا لـ :size.',
'file' => 'يجب أن يكون :attribute مساويًا لـ :size كيلوبايت.',
'string' => 'يجب أن يكون :attribute مساويًا لـ :size حرفًا.',
'array' => 'يجب أن يحتوي :attribute على :size عنصر.',
],
'starts_with' => 'يجب أن يبدأ :attribute بأحد القيم التالية: :values.',
'string' => 'يجب أن يكون :attribute نصًا.',
'timezone' => 'يجب أن يكون :attribute منطقة زمنية صالحة.',
'unique' => 'تم استخدام :attribute مسبقًا.',
'uploaded' => 'فشل تحميل :attribute.',
'url' => 'يجب أن يكون :attribute عنوان URL صالحًا.',
'uuid' => 'يجب أن يكون :attribute UUID صالحًا.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'رسالة مخصصة',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];

View File

@@ -1,20 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Estas credenciales no coinciden con nuestros registros.',
'password' => 'La contraseña proporcionada es incorrecta.',
'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, intenta de nuevo en :seconds segundos.',
];

View File

@@ -1,19 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Anterior',
'next' => 'Siguiente &raquo;',
];

View File

@@ -1,22 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'reset' => '¡Tu contraseña ha sido restablecida!',
'sent' => '¡Te hemos enviado por correo el enlace para restablecer tu contraseña!',
'throttled' => 'Por favor espera antes de volver a intentar.',
'token' => 'Este token de restablecimiento de contraseña es inválido.',
'user' => "No podemos encontrar un usuario con esa dirección de correo electrónico.",
];

View File

@@ -1,163 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'El campo :attribute debe ser aceptado.',
'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other sea :value.',
'active_url' => 'El campo :attribute no es una URL válida.',
'after' => 'El campo :attribute debe ser una fecha posterior a :date.',
'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.',
'alpha' => 'El campo :attribute solo debe contener letras.',
'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.',
'alpha_num' => 'El campo :attribute solo debe contener letras y números.',
'array' => 'El campo :attribute debe ser un arreglo.',
'before' => 'El campo :attribute debe ser una fecha anterior a :date.',
'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.',
'between' => [
'numeric' => 'El campo :attribute debe estar entre :min y :max.',
'file' => 'El campo :attribute debe estar entre :min y :max kilobytes.',
'string' => 'El campo :attribute debe tener entre :min y :max caracteres.',
'array' => 'El campo :attribute debe tener entre :min y :max elementos.',
],
'boolean' => 'El campo :attribute debe ser verdadero o falso.',
'confirmed' => 'La confirmación de :attribute no coincide.',
'current_password' => 'La contraseña es incorrecta.',
'date' => 'El campo :attribute no es una fecha válida.',
'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.',
'date_format' => 'El campo :attribute no coincide con el formato :format.',
'declined' => 'El campo :attribute debe ser rechazado.',
'declined_if' => 'El campo :attribute debe ser rechazado cuando :other sea :value.',
'different' => 'El campo :attribute y :other deben ser diferentes.',
'digits' => 'El campo :attribute debe tener :digits dígitos.',
'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.',
'dimensions' => 'El campo :attribute tiene dimensiones de imagen inválidas.',
'distinct' => 'El campo :attribute tiene un valor duplicado.',
'email' => 'El campo :attribute debe ser una dirección de correo electrónico válida.',
'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes: :values.',
'enum' => 'El :attribute seleccionado es inválido.',
'exists' => 'El :attribute seleccionado es inválido.',
'file' => 'El campo :attribute debe ser un archivo.',
'filled' => 'El campo :attribute debe tener un valor.',
'gt' => [
'numeric' => 'El campo :attribute debe ser mayor que :value.',
'file' => 'El campo :attribute debe ser mayor que :value kilobytes.',
'string' => 'El campo :attribute debe ser mayor que :value caracteres.',
'array' => 'El campo :attribute debe tener más de :value elementos.',
],
'gte' => [
'numeric' => 'El campo :attribute debe ser mayor o igual a :value.',
'file' => 'El campo :attribute debe ser mayor o igual a :value kilobytes.',
'string' => 'El campo :attribute debe ser mayor o igual a :value caracteres.',
'array' => 'El campo :attribute debe tener :value elementos o más.',
],
'image' => 'El campo :attribute debe ser una imagen.',
'in' => 'El :attribute seleccionado es inválido.',
'in_array' => 'El campo :attribute no existe en :other.',
'integer' => 'El campo :attribute debe ser un número entero.',
'ip' => 'El campo :attribute debe ser una dirección IP válida.',
'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.',
'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.',
'json' => 'El campo :attribute debe ser una cadena JSON válida.',
'lt' => [
'numeric' => 'El campo :attribute debe ser menor que :value.',
'file' => 'El campo :attribute debe ser menor que :value kilobytes.',
'string' => 'El campo :attribute debe ser menor que :value caracteres.',
'array' => 'El campo :attribute debe tener menos de :value elementos.',
],
'lte' => [
'numeric' => 'El campo :attribute debe ser menor o igual a :value.',
'file' => 'El campo :attribute debe ser menor o igual a :value kilobytes.',
'string' => 'El campo :attribute debe ser menor o igual a :value caracteres.',
'array' => 'El campo :attribute no debe tener más de :value elementos.',
],
'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.',
'max' => [
'numeric' => 'El campo :attribute no debe ser mayor que :max.',
'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.',
'string' => 'El campo :attribute no debe ser mayor que :max caracteres.',
'array' => 'El campo :attribute no debe tener más de :max elementos.',
],
'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
'min' => [
'numeric' => 'El campo :attribute debe ser al menos :min.',
'file' => 'El campo :attribute debe ser al menos :min kilobytes.',
'string' => 'El campo :attribute debe tener al menos :min caracteres.',
'array' => 'El campo :attribute debe tener al menos :min elementos.',
],
'multiple_of' => 'El campo :attribute debe ser un múltiplo de :value.',
'not_in' => 'El :attribute seleccionado es inválido.',
'not_regex' => 'El formato del campo :attribute es inválido.',
'numeric' => 'El campo :attribute debe ser un número.',
'password' => 'La contraseña es incorrecta.',
'present' => 'El campo :attribute debe estar presente.',
'prohibited' => 'El campo :attribute está prohibido.',
'prohibited_if' => 'El campo :attribute está prohibido cuando :other sea :value.',
'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.',
'prohibits' => 'El campo :attribute prohíbe que :other esté presente.',
'regex' => 'El formato del campo :attribute es inválido.',
'required' => 'El campo :attribute es obligatorio.',
'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.',
'required_if' => 'El campo :attribute es obligatorio cuando :other sea :value.',
'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.',
'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.',
'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.',
'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values están presentes.',
'same' => 'El campo :attribute y :other deben coincidir.',
'size' => [
'numeric' => 'El campo :attribute debe ser :size.',
'file' => 'El campo :attribute debe ser :size kilobytes.',
'string' => 'El campo :attribute debe tener :size caracteres.',
'array' => 'El campo :attribute debe contener :size elementos.',
],
'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes: :values.',
'string' => 'El campo :attribute debe ser una cadena de texto.',
'timezone' => 'El campo :attribute debe ser una zona horaria válida.',
'unique' => 'El campo :attribute ya ha sido tomado.',
'uploaded' => 'El campo :attribute falló al subir.',
'url' => 'El campo :attribute debe ser una URL válida.',
'uuid' => 'El campo :attribute debe ser un UUID válido.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];

View File

@@ -1,15 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان احراز هویت
|--------------------------------------------------------------------------
|
| خطوط زبان زیر در طول احراز هویت برای پیام‌های مختلفی که باید به کاربر نمایش دهیم استفاده می‌شوند.
| شما می‌توانید این خطوط زبان را بر اساس نیازهای برنامه خود تغییر دهید.
|
*/
'failed' => 'این اطلاعات ورود با سوابق ما مطابقت ندارد.',
'password' => 'رمز عبور ارائه‌شده نادرست است.',
'throttle' => 'تعداد تلاش‌های ورود بیش از حد زیاد است. لطفاً پس از :seconds ثانیه دوباره تلاش کنید.',
];

View File

@@ -1,18 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان صفحه‌بندی
|--------------------------------------------------------------------------
|
| خطوط زبان زیر توسط کتابخانه صفحه‌بندی برای ساخت لینک‌های صفحه‌بندی ساده استفاده می‌شوند.
| شما می‌توانید این خطوط را به دلخواه تغییر دهید تا با نیازهای برنامه خود سازگار شوند.
|
*/
'previous' => '&laquo; قبلی',
'next' => 'بعدی &raquo;',
];

View File

@@ -1,22 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان بازنشانی رمز عبور
|--------------------------------------------------------------------------
|
| خطوط زبان زیر خطوط پیش‌فرضی هستند که با دلایلی که توسط کارگزار رمز عبور
| برای تلاش‌های ناموفق به‌روزرسانی رمز عبور ارائه می‌شوند، مطابقت دارند،
| مانند توکن نامعتبر یا رمز عبور جدید نامعتبر.
|
*/
'reset' => 'رمز عبور شما بازنشانی شد!',
'sent' => 'لینک بازنشانی رمز عبور به ایمیل شما ارسال شد!',
'throttled' => 'لطفاً قبل از تلاش مجدد صبر کنید.',
'token' => 'این توکن بازنشانی رمز عبور نامعتبر است.',
'user' => 'کاربری با این آدرس ایمیل یافت نشد.',
];

View File

@@ -1,163 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان اعتبارسنجی
|--------------------------------------------------------------------------
|
| خطوط زبان زیر شامل پیام‌های خطای پیش‌فرض استفاده‌شده توسط کلاس اعتبارسنجی هستند.
| برخی از این قوانین نسخه‌های متعددی دارند، مانند قوانین مربوط به اندازه.
| شما می‌توانید این پیام‌ها را در اینجا به دلخواه تنظیم کنید.
|
*/
'accepted' => 'فیلد :attribute باید پذیرفته شود.',
'accepted_if' => 'فیلد :attribute باید پذیرفته شود وقتی :other برابر با :value باشد.',
'active_url' => 'فیلد :attribute یک URL معتبر نیست.',
'after' => 'فیلد :attribute باید تاریخی پس از :date باشد.',
'after_or_equal' => 'فیلد :attribute باید تاریخی پس از یا برابر با :date باشد.',
'alpha' => 'فیلد :attribute فقط باید شامل حروف باشد.',
'alpha_dash' => 'فیلد :attribute فقط باید شامل حروف، اعداد، خط تیره و زیرخط باشد.',
'alpha_num' => 'فیلد :attribute فقط باید شامل حروف و اعداد باشد.',
'array' => 'فیلد :attribute باید یک آرایه باشد.',
'before' => 'فیلد :attribute باید تاریخی قبل از :date باشد.',
'before_or_equal' => 'فیلد :attribute باید تاریخی قبل از یا برابر با :date باشد.',
'between' => [
'numeric' => 'فیلد :attribute باید بین :min و :max باشد.',
'file' => 'فیلد :attribute باید بین :min و :max کیلوبایت باشد.',
'string' => 'فیلد :attribute باید بین :min و :max کاراکتر باشد.',
'array' => 'فیلد :attribute باید بین :min و :max آیتم داشته باشد.',
],
'boolean' => 'فیلد :attribute باید true یا false باشد.',
'confirmed' => 'تأیید فیلد :attribute مطابقت ندارد.',
'current_password' => 'رمز عبور نادرست است.',
'date' => 'فیلد :attribute یک تاریخ معتبر نیست.',
'date_equals' => 'فیلد :attribute باید تاریخی برابر با :date باشد.',
'date_format' => 'فیلد :attribute با فرمت :format مطابقت ندارد.',
'declined' => 'فیلد :attribute باید رد شود.',
'declined_if' => 'فیلد :attribute باید رد شود وقتی :other برابر با :value باشد.',
'different' => 'فیلد :attribute و :other باید متفاوت باشند.',
'digits' => 'فیلد :attribute باید :digits رقم باشد.',
'digits_between' => 'فیلد :attribute باید بین :min و :max رقم باشد.',
'dimensions' => 'فیلد :attribute دارای ابعاد تصویر نامعتبر است.',
'distinct' => 'فیلد :attribute دارای مقدار تکراری است.',
'email' => 'فیلد :attribute باید یک آدرس ایمیل معتبر باشد.',
'ends_with' => 'فیلد :attribute باید با یکی از مقادیر زیر پایان یابد: :values.',
'enum' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'exists' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'file' => 'فیلد :attribute باید یک فایل باشد.',
'filled' => 'فیلد :attribute باید دارای مقدار باشد.',
'gt' => [
'numeric' => 'فیلد :attribute باید بزرگ‌تر از :value باشد.',
'file' => 'فیلد :attribute باید بزرگ‌تر از :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید بیش از :value کاراکتر باشد.',
'array' => 'فیلد :attribute باید بیش از :value آیتم داشته باشد.',
],
'gte' => [
'numeric' => 'فیلد :attribute باید بزرگ‌تر یا برابر با :value باشد.',
'file' => 'فیلد :attribute باید بزرگ‌تر یا برابر با :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید بیش از یا برابر با :value کاراکتر باشد.',
'array' => 'فیلد :attribute باید :value آیتم یا بیشتر داشته باشد.',
],
'image' => 'فیلد :attribute باید یک تصویر باشد.',
'in' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'in_array' => 'فیلد :attribute در :other وجود ندارد.',
'integer' => 'فیلد :attribute باید یک عدد صحیح باشد.',
'ip' => 'فیلد :attribute باید یک آدرس IP معتبر باشد.',
'ipv4' => 'فیلد :attribute باید یک آدرس IPv4 معتبر باشد.',
'ipv6' => 'فیلد :attribute باید یک آدرس IPv6 معتبر باشد.',
'json' => 'فیلد :attribute باید یک رشته JSON معتبر باشد.',
'lt' => [
'numeric' => 'فیلد :attribute باید کمتر از :value باشد.',
'file' => 'فیلد :attribute باید کمتر از :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید کمتر از :value کاراکتر باشد.',
'array' => 'فیلد :attribute باید کمتر از :value آیتم داشته باشد.',
],
'lte' => [
'numeric' => 'فیلد :attribute باید کمتر یا برابر با :value باشد.',
'file' => 'فیلد :attribute باید کمتر یا برابر با :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید کمتر یا برابر با :value کاراکتر باشد.',
'array' => 'فیلد :attribute نباید بیش از :value آیتم داشته باشد.',
],
'mac_address' => 'فیلد :attribute باید یک آدرس MAC معتبر باشد.',
'max' => [
'numeric' => 'فیلد :attribute نباید بزرگ‌تر از :max باشد.',
'file' => 'فیلد :attribute نباید بزرگ‌تر از :max کیلوبایت باشد.',
'string' => 'فیلد :attribute نباید بیش از :max کاراکتر باشد.',
'array' => 'فیلد :attribute نباید بیش از :max آیتم داشته باشد.',
],
'mimes' => 'فیلد :attribute باید یک فایل از نوع: :values باشد.',
'mimetypes' => 'فیلد :attribute باید یک فایل از نوع: :values باشد.',
'min' => [
'numeric' => 'فیلد :attribute باید حداقل :min باشد.',
'file' => 'فیلد :attribute باید حداقل :min کیلوبایت باشد.',
'string' => 'فیلد :attribute باید حداقل :min کاراکتر باشد.',
'array' => 'فیلد :attribute باید حداقل :min آیتم داشته باشد.',
],
'multiple_of' => 'فیلد :attribute باید مضربی از :value باشد.',
'not_in' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'not_regex' => 'فرمت فیلد :attribute نامعتبر است.',
'numeric' => 'فیلد :attribute باید یک عدد باشد.',
'password' => 'رمز عبور نادرست است.',
'present' => 'فیلد :attribute باید وجود داشته باشد.',
'prohibited' => 'فیلد :attribute ممنوع است.',
'prohibited_if' => 'فیلد :attribute وقتی :other برابر با :value باشد ممنوع است.',
'prohibited_unless' => 'فیلد :attribute ممنوع است مگر اینکه :other در :values باشد.',
'prohibits' => 'فیلد :attribute مانع حضور :other می‌شود.',
'regex' => 'فرمت فیلد :attribute نامعتبر است.',
'required' => 'فیلد :attribute الزامی است.',
'required_array_keys' => 'فیلد :attribute باید شامل ورودی‌هایی برای: :values باشد.',
'required_if' => 'فیلد :attribute وقتی :other برابر با :value باشد الزامی است.',
'required_unless' => 'فیلد :attribute الزامی است مگر اینکه :other در :values باشد.',
'required_with' => 'فیلد :attribute وقتی :values وجود دارد الزامی است.',
'required_with_all' => 'فیلد :attribute وقتی همه :values وجود دارند الزامی است.',
'required_without' => 'فیلد :attribute وقتی :values وجود ندارد الزامی است.',
'required_without_all' => 'فیلد :attribute وقتی هیچ‌کدام از :values وجود ندارند الزامی است.',
'same' => 'فیلد :attribute و :other باید یکسان باشند.',
'size' => [
'numeric' => 'فیلد :attribute باید :size باشد.',
'file' => 'فیلد :attribute باید :size کیلوبایت باشد.',
'string' => 'فیلد :attribute باید :size کاراکتر باشد.',
'array' => 'فیلد :attribute باید شامل :size آیتم باشد.',
],
'starts_with' => 'فیلد :attribute باید با یکی از مقادیر زیر شروع شود: :values.',
'string' => 'فیلد :attribute باید یک رشته باشد.',
'timezone' => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.',
'unique' => 'فیلد :attribute قبلاً استفاده شده است.',
'uploaded' => 'فیلد :attribute در آپلود ناموفق بود.',
'url' => 'فیلد :attribute باید یک URL معتبر باشد.',
'uuid' => 'فیلد :attribute باید یک UUID معتبر باشد.',
/*
|--------------------------------------------------------------------------
| خطوط زبان اعتبارسنجی سفارشی
|--------------------------------------------------------------------------
|
| در اینجا می‌توانید پیام‌های اعتبارسنجی سفارشی برای ویژگی‌ها را با استفاده از
| قرارداد "attribute.rule" برای نام‌گذاری خطوط مشخص کنید. این کار امکان
| تعیین سریع یک خط زبان سفارشی برای یک قانون خاص ویژگی را فراهم می‌کند.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'پیام سفارشی',
],
],
/*
|--------------------------------------------------------------------------
| ویژگی‌های اعتبارسنجی سفارشی
|--------------------------------------------------------------------------
|
| خطوط زبان زیر برای جایگزینی placeholder ویژگی‌های ما با چیزی کاربرپسندتر
| مانند "آدرس ایمیل" به جای "email" استفاده می‌شوند. این کار به ما کمک می‌کند
| پیام‌هایمان را گویاتر کنیم.
|
*/
'attributes' => [],
];

View File

@@ -1,20 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Ushbu hisobga olish maʼlumotlari bizning yozuvlarimizga mos kelmadi.',
'password' => 'Taqdim etilgan parol notogri.',
'throttle' => 'Kirishga urinishlar juda koʻp. Iltimos, :soniyadan keyin qayta urinib koʻring.',
];

View File

@@ -1,19 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Oldingi',
'next' => 'Keyingi &raquo;',
];

View File

@@ -1,22 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'reset' => 'Sizning parolingiz tiklandi!',
'sent' => 'Parolni tiklash havolasini elektron pochta orqali yubordik!',
'throttled' => 'Qayta urinishdan oldin kuting.',
'token' => 'Ushbu parolni tiklash belgisi yaroqsiz.',
'user' => "Biz bu elektron pochta manziliga ega foydalanuvchini topa olmadik.",
];

View File

@@ -1,163 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => ':attribute qabul qilinishi kerak.',
'accepted_if' => ':other :value bolsa, :attribute qabul qilinishi kerak.',
'active_url' => ':attribute yaroqli URL emas.',
'after' => ':attribute :date dan keyingi sana boʻlishi kerak.',
'after_or_equal' => ':attribute :date ga teng yoki undan keyingi sana boʻlishi kerak.',
'alpha' => ':attribute faqat harflardan iborat boʻlishi kerak.',
'alpha_dash' => ':attribute faqat harflar, raqamlar, chiziqlar va pastki chiziqlardan iborat boʻlishi kerak.',
'alpha_num' => ':attribute faqat harflar va raqamlardan iborat boʻlishi kerak.',
'array' => ':attribute massiv boʻlishi kerak.',
'before' => ':attribute :date gacha boʻlgan sana boʻlishi kerak.',
'before_or_equal' => ':attribute :date ga teng yoki undan oldingi sana boʻlishi kerak.',
'between' => [
'numeric' => ':attribute :min va :max orasida boʻlishi kerak.',
'file' => ':attribute :min va :max kilobayt orasida boʻlishi kerak.',
'string' => ':attribute :min va :max belgilar orasida boʻlishi kerak.',
'array' => ':attribute :min va :max ta element orasida boʻlishi kerak.',
],
'boolean' => ':attribute maydoni rost yoki yolgʻon boʻlishi kerak.',
'confirmed' => ':attribute tasdiqlanishi mos kelmadi.',
'current_password' => 'Parol notogri.',
'date' => ':attribute yaroqli sana emas.',
'date_equals' => ':attribute :date ga teng sana boʻlishi kerak.',
'date_format' => ':attribute :format formatiga mos kelmadi.',
'declined' => ':attribute rad etilishi kerak.',
'declined_if' => ':other :value bolsa, :attribute rad etilishi kerak.',
'different' => ':attribute va :other har xil boʻlishi kerak.',
'digits' => ':attribute :digits raqamdan iborat boʻlishi kerak.',
'digits_between' => ':attribute :min va :max raqamlari orasida boʻlishi kerak.',
'dimensions' => ':attribute rasm oʻlchamlari yaroqsiz.',
'distinct' => ':attribute maydonida takroriy qiymat mavjud.',
'email' => ':attribute yaroqli elektron pochta manzili boʻlishi kerak.',
'ends_with' => ':attribute quyidagilardan biri bilan tugashi kerak: :values.',
'enum' => 'Tanlangan :attribute yaroqsiz.',
'exists' => 'Tanlangan :attribute yaroqsiz.',
'file' => ':attribute fayl boʻlishi kerak.',
'filled' => ':attribute maydonida qiymat boʻlishi kerak.',
'gt' => [
'numeric' => ':attribute :value dan katta boʻlishi kerak.',
'file' => ':attribute :value kilobaytdan katta boʻlishi kerak.',
'string' => ':attribute :value belgidan katta boʻlishi kerak.',
'array' => ':attribute :value dan koʻp elementga ega boʻlishi kerak.',
],
'gte' => [
'numeric' => ':attribute :value ga teng yoki undan katta boʻlishi kerak.',
'file' => ':attribute :value kilobaytga teng yoki undan katta boʻlishi kerak.',
'string' => ':attribute :value belgiga teng yoki undan katta boʻlishi kerak.',
'array' => ':attribute :value yoki undan koʻp elementga ega boʻlishi kerak.',
],
'image' => ':attribute rasm boʻlishi kerak.',
'in' => 'Tanlangan :attribute yaroqsiz.',
'in_array' => ':attribute maydoni :other da mavjud emas.',
'integer' => ':attribute butun son boʻlishi kerak.',
'ip' => ':attribute yaroqli IP manzil boʻlishi kerak.',
'ipv4' => ':attribute yaroqli IPv4 manzil boʻlishi kerak.',
'ipv6' => ':attribute yaroqli IPv6 manzil boʻlishi kerak.',
'json' => ':attribute yaroqli JSON qatori boʻlishi kerak.',
'lt' => [
'numeric' => ':attribute :value dan kichik boʻlishi kerak.',
'file' => ':attribute :value kilobaytdan kichik boʻlishi kerak.',
'string' => ':attribute :value belgidan kichik boʻlishi kerak.',
'array' => ':attribute :value dan kam elementga ega boʻlishi kerak.',
],
'lte' => [
'numeric' => ':attribute :value ga teng yoki undan kichik boʻlishi kerak.',
'file' => ':attribute :value kilobaytga teng yoki undan kichik boʻlishi kerak.',
'string' => ':attribute :value belgiga teng yoki undan kichik boʻlishi kerak.',
'array' => ':attribute :value dan koʻp boʻlmagan elementga ega boʻlishi kerak.',
],
'mac_address' => ':attribute yaroqli MAC manzil boʻlishi kerak.',
'max' => [
'numeric' => ':attribute :max dan katta boʻlmasligi kerak.',
'file' => ':attribute :max kilobaytdan katta boʻlmasligi kerak.',
'string' => ':attribute :max belgidan katta boʻlmasligi kerak.',
'array' => ':attribute :max dan koʻp boʻlmagan elementga ega boʻlishi kerak.',
],
'mimes' => ':attribute :values turidagi fayl boʻlishi kerak.',
'mimetypes' => ':attribute :values turidagi fayl boʻlishi kerak.',
'min' => [
'numeric' => ':attribute kamida :min boʻlishi kerak.',
'file' => ':attribute kamida :min kilobayt boʻlishi kerak.',
'string' => ':attribute kamida :min belgidan iborat boʻlishi kerak.',
'array' => ':attribute kamida :min ta elementga ega boʻlishi kerak.',
],
'multiple_of' => ':attribute :value ning karrali boʻlishi kerak.',
'not_in' => 'Tanlangan :attribute yaroqsiz.',
'not_regex' => ':attribute formati yaroqsiz.',
'numeric' => ':attribute raqam boʻlishi kerak.',
'password' => 'Parol notogri.',
'present' => ':attribute maydoni mavjud boʻlishi kerak.',
'prohibited' => ':attribute maydoni taqiqlangan.',
'prohibited_if' => ':other :value bolsa, :attribute maydoni taqiqlangan.',
'prohibited_unless' => ':other :values da boʻlmasa, :attribute maydoni taqiqlangan.',
'prohibits' => ':attribute maydoni :other ning mavjud boʻlishini taqiqlaydi.',
'regex' => ':attribute formati yaroqsiz.',
'required' => ':attribute maydoni toʻldirilishi shart.',
'required_array_keys' => ':attribute maydonida :values uchun yozuvlar boʻlishi kerak.',
'required_if' => ':other :value bolsa, :attribute maydoni toʻldirilishi shart.',
'required_unless' => ':other :values da boʻlmasa, :attribute maydoni toʻldirilishi shart.',
'required_with' => ':values mavjud boʻlganda :attribute maydoni toʻldirilishi shart.',
'required_with_all' => ':values mavjud boʻlganda :attribute maydoni toʻldirilishi shart.',
'required_without' => ':values mavjud boʻlmaganda :attribute maydoni toʻldirilishi shart.',
'required_without_all' => ':values ning hech biri mavjud boʻlmaganda :attribute maydoni toʻldirilishi shart.',
'same' => ':attribute va :other mos kelishi kerak.',
'size' => [
'numeric' => ':attribute :size boʻlishi kerak.',
'file' => ':attribute :size kilobayt boʻlishi kerak.',
'string' => ':attribute :size belgidan iborat boʻlishi kerak.',
'array' => ':attribute :size ta elementdan iborat boʻlishi kerak.',
],
'starts_with' => ':attribute quyidagilardan biri bilan boshlanishi kerak: :values.',
'string' => ':attribute qator boʻlishi kerak.',
'timezone' => ':attribute yaroqli vaqt mintaqasi boʻlishi kerak.',
'unique' => ':attribute allaqachon olingan.',
'uploaded' => ':attribute yuklab olinmadi.',
'url' => ':attribute yaroqli URL boʻlishi kerak.',
'uuid' => ':attribute yaroqli UUID boʻlishi kerak.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];

View File

@@ -1,47 +0,0 @@
#!/bin/bash
set -e
# Resolve the directory the script is located in
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
APP_NAME="Fleetbase"
IMAGE_NAME="fleetbase-linux-static"
CONTAINER_NAME="fleetbase-linux-build"
DIST_DIR="$ROOT_DIR/builds/dist"
BINARY_NAME="fleetbase-linux-x86_64"
DOCKERFILE="$ROOT_DIR/builds/linux/static-build.Dockerfile"
# Ensure pkg-config archive is available
SPC_DOWNLOADS_DIR="$SCRIPT_DIR/spc/downloads"
PKG_TAR="pkg-config-0.29.2.tar.gz"
PKG_URL="https://static-php-cli.fra1.digitaloceanspaces.com/static-php-cli/deps/pkg-config/${PKG_TAR}"
if [[ ! -f "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" ]]; then
echo "📥 pkg-config archive missing downloading..."
mkdir -p "${SPC_DOWNLOADS_DIR}"
curl -L --retry 3 -o "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" "${PKG_URL}"
else
echo "✅ pkg-config archive already present."
fi
# Build the image
echo "📦 Building static Linux binary for ${APP_NAME}..."
docker build -f "$DOCKERFILE" -t "$IMAGE_NAME" .
# Create a container from the built image
echo "📦 Creating container to extract binary..."
docker create --name "$CONTAINER_NAME" "$IMAGE_NAME"
# Make sure dist folder exist
mkdir -p "$DIST_DIR"
# Copy binary from container to local dist folder
echo "📂 Extracting binary..."
docker cp "$CONTAINER_NAME:/go/src/app/dist/frankenphp-linux-x86_64" "$DIST_DIR/$BINARY_NAME"
# Cleanup the temp container
docker rm "$CONTAINER_NAME"
echo "✅ Build complete! Binary is located at: $DIST_DIR/$BINARY_NAME"

View File

@@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\linux\library;
class libgeos extends LinuxLibraryBase
{
use \SPC\builder\unix\library\libgeos;
public const NAME = 'libgeos';
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
trait libgeos
{
/**
* @throws FileSystemException
* @throws RuntimeException
*/
protected function build(): void
{
FileSystem::resetDir($this->source_dir . '/build');
shell()->cd($this->source_dir . '/build')
->setEnv([
'CFLAGS' => $this->getLibExtraCFlags(),
'LDFLAGS' => $this->getLibExtraLdFlags(),
'LIBS' => $this->getLibExtraLibs(),
])
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
->execWithEnv("make -j{$this->builder->concurrency}")
->execWithEnv('make install');
$this->patchPkgconfPrefix(['geos.pc']);
}
}

View File

@@ -1,93 +0,0 @@
# FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 docker.io/dunglas/frankenphp:static-builder@sha256:821526b776a26502735d83890cc0a0d579348c510ba6c777df0762cb1c50d967
WORKDIR /go/src/app
# Copy Fleetbase app
COPY ../../api ./dist/app
# Set working directory to the embedded Fleetbase app
WORKDIR /go/src/app/dist/app
# Setup for production environment
ENV APP_ENV=production
ENV APP_DEBUG=false
ENV BROADCAST_DRIVER=socketcluster
ENV OSRM_HOST="https://router.project-osrm.org"
ENV REGISTRY_PREINSTALLED_EXTENSIONS=true
# Optional: Ensure writable storage
RUN chmod -R 775 bootstrap/cache storage
# Set permissions for deploy script
RUN chmod +x ./deploy.sh
# Move back to main app directory before running build-static.sh
WORKDIR /go/src/app
# Install geos lib
RUN apk add --no-cache geos geos-dev
# Inject the libgeos library handlers
COPY ./builds/linux/spc/libgeos-linux.php ./dist/static-php-cli/src/SPC/builder/linux/library/libgeos.php
COPY ./builds/linux/spc/libgeos-unix.php ./dist/static-php-cli/src/SPC/builder/unix/library/libgeos.php
# Patch source.json to add geos extension source
RUN jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
# Pathc source.json to add libgeos library
RUN jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
# Patch ext.json to add geos extension dynamically
RUN jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
./dist/static-php-cli/config/ext.json > ./dist/static-php-cli/config/ext.tmp.json && \
mv ./dist/static-php-cli/config/ext.tmp.json ./dist/static-php-cli/config/ext.json
# Patch lib.json to add libgeos
RUN jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
./dist/static-php-cli/config/lib.json > ./dist/static-php-cli/config/lib.tmp.json && \
mv ./dist/static-php-cli/config/lib.tmp.json ./dist/static-php-cli/config/lib.json
# Install dependencies for SPC CLI
WORKDIR /go/src/app/dist/static-php-cli
RUN composer install --no-dev -a
# Set PHP extensions to be built (including geos!)
ENV PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
ENV PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
# Force SPC to use the local source version (not download binary)
ENV SPC_REL_TYPE=source
# Debug build
ENV SPC_LOG_LEVEL=debug
# Skip compression
ENV NO_COMPRESS=1
# set PHP version
ENV PHP_VERSION=8.2
# Move to the app directory
WORKDIR /go/src/app
# Make sure pkg-config is available within the static build container
COPY ./builds/linux/spc/downloads/pkg-config-0.29.2.tar.gz ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz
# Pre-build pkg-config using the existing tarball
RUN apk add --no-cache build-base && \
tar -xzf ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz -C /tmp && \
cd /tmp/pkg-config-0.29.2 && \
./configure --with-internal-glib --prefix=/go/src/app/dist/static-php-cli/build/bin && \
make && make install && \
rm -rf /tmp/pkg-config-0.29.2
# Do not run git pull
RUN sed -i 's/^[ \t]*git pull/# git pull/' ./build-static.sh
# Build the FrankenPHP static binary
RUN EMBED=dist/app ./build-static.sh

View File

@@ -1,188 +0,0 @@
#!/bin/bash
set -e
log() {
echo -e "\033[1;34m[🔧 $1]\033[0m"
}
log_success() {
echo -e "\033[1;32m[✅ $1]\033[0m"
}
log_warn() {
echo -e "\033[1;33m[⚠️ $1]\033[0m"
}
log_error() {
echo -e "\033[1;31m[❌ $1]\033[0m"
}
# Define base paths
log "Resolving directories..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
OSX_DIR="$ROOT_DIR/builds/osx"
DIST_DIR="$ROOT_DIR/builds/dist"
APP_DIR="$ROOT_DIR/api"
BREW_PREFIX="/opt/homebrew"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
BINARY_NAME="fleetbase-$OS-$ARCH"
log "Binary will be: $BINARY_NAME"
# Setup PHP 8.4
log "Detecting current PHP version..."
ORIGINAL_PHP_PATH="$(which php)"
ORIGINAL_PHP_VERSION="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION.".".PHP_RELEASE_VERSION;' 2>/dev/null)"
IS_ASDF_MANAGED=false
if [[ "$ORIGINAL_PHP_PATH" == *".asdf"* ]]; then
IS_ASDF_MANAGED=true
fi
# 🔁 Trap to restore PHP when script exits
trap 'if [ "$IS_ASDF_MANAGED" = true ]; then
log "Restoring asdf-managed PHP version: $ORIGINAL_PHP_VERSION"
asdf set php "$ORIGINAL_PHP_VERSION" || true
log "Reverted to PHP $(php -v | head -n 1)"
else
log "Unsetting asdf set to restore system PHP"
asdf set php system || true
log "Reverted to PHP $(php -v | head -n 1)"
fi' EXIT
log "Detected PHP version: $ORIGINAL_PHP_VERSION"
log "Detected PHP binary: $ORIGINAL_PHP_PATH"
# ───────────────────────────────────────────────────────────────────────────────
# If the *current* php is already 8.4.x, we skip the entire asdf install step
# ───────────────────────────────────────────────────────────────────────────────
if [[ "$ORIGINAL_PHP_PATH" == "$BREW_PREFIX/bin/php" && "$ORIGINAL_PHP_VERSION" =~ ^8\.4\. ]]; then
log "Homebrew PHP $ORIGINAL_PHP_VERSION detected at $ORIGINAL_PHP_PATH — skipping asdf build/install."
else
# Only install under asdf if we dont already have 8.4.0 installed
log "No Homebrew PHP 8.4 detected (found $ORIGINAL_PHP_PATH $ORIGINAL_PHP_VERSION), using asdf to build/install."
if ! asdf list php | grep -q "8.4.0"; then
# Use brew to install required dependencies for asdf php management
log "Checking and installing Homebrew packages required for PHP 8.4 build..."
for pkg in autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip pkg-config re2c zlib sqlite3 libsodium oniguruma openssl@3 nasm; do
if ! brew list "$pkg" &>/dev/null; then
log_warn "$pkg not found. Installing..."
arch -arm64 brew install "$pkg"
else
log "$pkg already installed. Skipping."
fi
done
# Set necessary env flags/paths for PHP build on OSX ARM64
export CPPFLAGS="-I$BREW_PREFIX/opt/oniguruma/include -I$BREW_PREFIX/opt/libsodium/include -I$BREW_PREFIX/opt/bzip2/include -I$BREW_PREFIX/opt/zlib/include -I$BREW_PREFIX/opt/openssl@3/include -I$BREW_PREFIX/opt/libxml2/include -I$BREW_PREFIX/opt/libedit/include -I$BREW_PREFIX/opt/curl/include -I$BREW_PREFIX/opt/sqlite3/include -I$BREW_PREFIX/opt/freetype/include -I$BREW_PREFIX/opt/jpeg/include -I$BREW_PREFIX/opt/libpng/include -I$BREW_PREFIX/opt/libzip/include"
export LDFLAGS="-L$BREW_PREFIX/opt/openssl@3/lib -lssl -lcrypto -lz -L$BREW_PREFIX/opt/oniguruma/lib -lonig -L$BREW_PREFIX/opt/libsodium/lib -lsodium -L$BREW_PREFIX/opt/bzip2/lib -Wl,-rpath,$BREW_PREFIX/opt/bzip2/lib -lbz2 -L$BREW_PREFIX/opt/zlib/lib -L$BREW_PREFIX/opt/openssl@3/lib -L$BREW_PREFIX/opt/libxml2/lib -L$BREW_PREFIX/opt/libedit/lib -L$BREW_PREFIX/opt/sqlite3/lib -lsqlite3 -L$BREW_PREFIX/opt/curl/lib -lcurl -L$BREW_PREFIX/opt/freetype/lib -L$BREW_PREFIX/opt/jpeg/lib -L$BREW_PREFIX/opt/libpng/lib -L$BREW_PREFIX/opt/libzip/lib -lzip -lz"
export PKG_CONFIG_PATH="$BREW_PREFIX/opt/openssl/lib/pkgconfig:$BREW_PREFIX/opt/oniguruma/lib/pkgconfig:$BREW_PREFIX/opt/libsodium/lib/pkgconfig:$BREW_PREFIX/opt/libzip/lib/pkgconfig:$BREW_PREFIX/opt/gd/lib/pkgconfig:$BREW_PREFIX/opt/zlib/lib/pkgconfig:$BREW_PREFIX/opt/openssl@3/lib/pkgconfig:$BREW_PREFIX/opt/libxml2/lib/pkgconfig:$BREW_PREFIX/opt/curl/lib/pkgconfig:$BREW_PREFIX/opt/sqlite3/lib/pkgconfig:$BREW_PREFIX/opt/freetype/lib/pkgconfig:$BREW_PREFIX/opt/jpeg/lib/pkgconfig:$BREW_PREFIX/opt/libpng/lib/pkgconfig"
export PHP_CONFIGURE_OPTIONS="--with-openssl=$(brew --prefix openssl) --with-iconv=$(brew --prefix libiconv)"
log "Installing PHP 8.4.0 with asdf..."
asdf install php 8.4.0 --verbose
else
log "asdf already has PHP 8.4.0 installed, skipping"
fi
log "Switching to PHP 8.4.0 with asdf set..."
asdf set php 8.4.0 --home
fi
# Clone FrankenPHP
if [ ! -d "$OSX_DIR/frankenphp" ]; then
log "Cloning FrankenPHP..."
git clone https://github.com/dunglas/frankenphp "$OSX_DIR/frankenphp"
else
log_warn "FrankenPHP already cloned. Skipping."
fi
cd "$OSX_DIR/frankenphp"
# Patch build script
log "Patching build-static.sh to skip git pull..."
sed -i '' 's/^[ \t]*git pull/# git pull/' ./build-static.sh
# Set environment variables
log "Exporting build environment variables..."
export PHP_VERSION=8.2
export PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
export PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
export SPC_REL_TYPE=source
export NO_COMPRESS=1
export SPC_OPT_BUILD_ARGS="--debug"
export CMAKE_OSX_ARCHITECTURES=arm64
# Clone and prepare static-php-cli in dist/
STATIC_PHP_CLI_DIR="$OSX_DIR/frankenphp/dist/static-php-cli"
if [ ! -d "$STATIC_PHP_CLI_DIR" ]; then
log "Cloning static-php-cli into dist/..."
git clone --depth 1 --branch 2.5.2 https://github.com/crazywhalecc/static-php-cli.git "$STATIC_PHP_CLI_DIR"
else
log_warn "static-php-cli already exists in dist/. Skipping clone."
fi
# Inject libgeos support
log "Injecting libgeos patch files..."
cp "$ROOT_DIR/builds/osx/spc/libgeos-unix.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/library/libgeos.php"
cp "$ROOT_DIR/builds/osx/spc/libgeos-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/macos/library/libgeos.php"
cp "$ROOT_DIR/builds/osx/spc/UnixBuilderBase-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/UnixBuilderBase.php"
# Patch SPC config
log "Patching SPC config files (source.json, ext.json, lib.json)..."
jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
"$STATIC_PHP_CLI_DIR/config/lib.json" > "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" "$STATIC_PHP_CLI_DIR/config/lib.json"
jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
"$STATIC_PHP_CLI_DIR/config/ext.json" > "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" "$STATIC_PHP_CLI_DIR/config/ext.json"
# Prepare app embed folder
log "📦 Preparing embedded app directory..."
rm -rf ./dist/app
mkdir -p ./dist/app
cp -R "$APP_DIR"/* ./dist/app/
log "Patching build-static.sh to skip git pull and composer install..."
# Skip `git pull`
sed -i '' 's/^[[:space:]]*git pull/# git pull/' "$OSX_DIR/frankenphp/build-static.sh"
# Patch add CoreServices framework for Caddy build on OSX
sed -i '' 's/-framework CoreFoundation -framework SystemConfiguration/& -framework CoreServices/' "$OSX_DIR/frankenphp/build-static.sh"
# ── work around 403 on GH macOS runners ────────────────────────────────────────
log "Patching curl to use a browser-like User-Agent (to avoid 403s)…"
curl() {
command curl -sSL -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15" "$@"
}
export -f curl
# Build the binary
log "⚙️ Running FrankenPHP build-static.sh..."
EMBED=dist/app ./build-static.sh
# Move built binary to dist
log "Moving built binary to output folder..."
mkdir -p "$DIST_DIR"
mv dist/frankenphp-mac-$ARCH "$DIST_DIR/$BINARY_NAME"
log_success "✅ macOS binary built at: $DIST_DIR/$BINARY_NAME"
# Clean up frankenphp build and app embed folder
log "🧹 Cleaning temporary app directory..."
rm -rf "$OSX_DIR/frankenphp"

View File

@@ -1,271 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix;
use SPC\builder\BuilderBase;
use SPC\builder\linux\LinuxBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\util\DependencyUtil;
use SPC\util\SPCConfigUtil;
abstract class UnixBuilderBase extends BuilderBase
{
/** @var string cflags */
public string $arch_c_flags;
/** @var string C++ flags */
public string $arch_cxx_flags;
/** @var string cmake toolchain file */
public string $cmake_toolchain_file;
/**
* @throws WrongUsageException
* @throws FileSystemException
*/
public function getAllStaticLibFiles(): array
{
$libs = [];
// reorder libs
foreach ($this->libs as $lib) {
foreach ($lib->getDependencies() as $dep) {
$libs[] = $dep;
}
$libs[] = $lib;
}
$libFiles = [];
$libNames = [];
// merge libs
foreach ($libs as $lib) {
if (!in_array($lib::NAME, $libNames, true)) {
$libNames[] = $lib::NAME;
array_unshift($libFiles, ...$lib->getStaticLibs());
}
}
return array_map(fn ($x) => realpath(BUILD_LIB_PATH . "/{$x}"), $libFiles);
}
/**
* Return generic cmake options when configuring cmake projects
*/
public function makeCmakeArgs(): string
{
$extra = $this instanceof LinuxBuilder ? '-DCMAKE_C_COMPILER=' . getenv('CC') . ' ' : '';
// NEW: allow env-variable override
$arch = getenv('CMAKE_OSX_ARCHITECTURES') ?: 'arm64';
return $extra .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'-DCMAKE_INSTALL_BINDIR=bin ' .
'-DCMAKE_INSTALL_LIBDIR=lib ' .
'-DCMAKE_INSTALL_INCLUDEDIR=include ' .
"-DCMAKE_OSX_ARCHITECTURES={$arch} " .
"-DCMAKE_TOOLCHAIN_FILE={$this->cmake_toolchain_file}";
}
/**
* Generate configure flags
*/
public function makeAutoconfFlags(int $flag = AUTOCONF_ALL): string
{
$extra = '';
// TODO: add auto pkg-config support
if (($flag & AUTOCONF_LIBS) === AUTOCONF_LIBS) {
$extra .= 'LIBS="' . BUILD_LIB_PATH . '" ';
}
if (($flag & AUTOCONF_CFLAGS) === AUTOCONF_CFLAGS) {
$extra .= 'CFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
}
if (($flag & AUTOCONF_CPPFLAGS) === AUTOCONF_CPPFLAGS) {
$extra .= 'CPPFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
}
if (($flag & AUTOCONF_LDFLAGS) === AUTOCONF_LDFLAGS) {
$extra .= 'LDFLAGS="-L' . BUILD_LIB_PATH . '" ';
}
return $extra;
}
public function proveLibs(array $sorted_libraries): void
{
// search all supported libs
$support_lib_list = [];
$classes = FileSystem::getClassesPsr4(
ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library',
'SPC\builder\\' . osfamily2dir() . '\library'
);
foreach ($classes as $class) {
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
$support_lib_list[$class::NAME] = $class;
}
}
// if no libs specified, compile all supported libs
if ($sorted_libraries === [] && $this->isLibsOnly()) {
$libraries = array_keys($support_lib_list);
$sorted_libraries = DependencyUtil::getLibs($libraries);
}
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
}
$lib = new ($support_lib_list[$library])($this);
$this->addLib($lib);
}
// calculate and check dependencies
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
$this->lib_list = $sorted_libraries;
}
/**
* Sanity check after build complete
*
* @throws RuntimeException
*/
protected function sanityCheck(int $build_target): void
{
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('running cli sanity check');
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "echo \"hello\";"');
$raw_output = implode('', $output);
if ($ret !== 0 || trim($raw_output) !== 'hello') {
throw new RuntimeException("cli failed sanity check: ret[{$ret}]. out[{$raw_output}]");
}
foreach ($this->getExts(false) as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
$ext->runCliCheckUnix();
}
}
// sanity check for phpmicro
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
$test_task = $this->getMicroTestTasks();
foreach ($test_task as $task_name => $task) {
$test_file = SOURCE_PATH . '/' . $task_name . '.exe';
if (file_exists($test_file)) {
@unlink($test_file);
}
file_put_contents($test_file, file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') . $task['content']);
chmod($test_file, 0755);
[$ret, $out] = shell()->execWithResult($test_file);
foreach ($task['conditions'] as $condition => $closure) {
if (!$closure($ret, $out)) {
$raw_out = trim(implode('', $out));
throw new RuntimeException("micro failed sanity check: {$task_name}, condition [{$condition}], ret[{$ret}], out[{$raw_out}]");
}
}
}
}
// sanity check for embed
if (($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) {
logger()->info('running embed sanity check');
$sample_file_path = SOURCE_PATH . '/embed-test';
if (!is_dir($sample_file_path)) {
@mkdir($sample_file_path);
}
// copy embed test files
copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c');
copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php');
$util = new SPCConfigUtil($this);
$config = $util->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}";
if (PHP_OS_FAMILY === 'Linux' && getenv('SPC_LIBC') === 'musl') {
$lens .= ' -static';
}
[$ret, $out] = shell()->cd($sample_file_path)->execWithResult(getenv('CC') . ' -o embed embed.c ' . $lens);
if ($ret !== 0) {
throw new RuntimeException('embed failed sanity check: build failed. Error message: ' . implode("\n", $out));
}
// if someone changed to --enable-embed=shared, we need to add LD_LIBRARY_PATH
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
$ext_path = 'LD_LIBRARY_PATH=' . BUILD_ROOT_PATH . '/lib:$LD_LIBRARY_PATH ';
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.a');
} else {
$ext_path = '';
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.so');
}
[$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output));
}
}
}
/**
* 将编译好的二进制文件发布到 buildroot
*
* @param int $type 发布类型
* @throws RuntimeException
* @throws FileSystemException
*/
protected function deployBinary(int $type): bool
{
$src = match ($type) {
BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php',
BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx',
BUILD_TARGET_FPM => SOURCE_PATH . '/php-src/sapi/fpm/php-fpm',
default => throw new RuntimeException('Deployment does not accept type ' . $type),
};
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file');
FileSystem::createDir(BUILD_ROOT_PATH . '/bin');
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '/bin/'));
return true;
}
/**
* Run php clean
*
* @throws RuntimeException
*/
protected function cleanMake(): void
{
logger()->info('cleaning up');
shell()->cd(SOURCE_PATH . '/php-src')->exec('make clean');
}
/**
* Patch phpize and php-config if needed
* @throws FileSystemException
*/
protected function patchPhpScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
logger()->debug('Patching php-config prefix and libs order');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
}
}
}

View File

@@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\macos\library;
class libgeos extends MacOSLibraryBase
{
use \SPC\builder\unix\library\libgeos;
public const NAME = 'libgeos';
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
trait libgeos
{
/**
* @throws FileSystemException
* @throws RuntimeException
*/
protected function build(): void
{
FileSystem::resetDir($this->source_dir . '/build');
shell()->cd($this->source_dir . '/build')
->setEnv([
'CFLAGS' => $this->getLibExtraCFlags(),
'LDFLAGS' => $this->getLibExtraLdFlags(),
'LIBS' => $this->getLibExtraLibs(),
])
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
->execWithEnv("make -j{$this->builder->concurrency}")
->execWithEnv('make install');
$this->patchPkgconfPrefix(['geos.pc']);
}
}

4
console/.gitignore vendored
View File

@@ -26,7 +26,3 @@
# broccoli-debug
/DEBUG/
# Auto-generated extension files
/app/extensions/
/app/utils/extension-loaders.js

View File

@@ -6,19 +6,19 @@ WORKDIR /console
# Create the pnpm directory and set the PNPM_HOME environment variable
RUN mkdir -p ~/.pnpm
ENV PNPM_HOME=/root/.pnpm
ENV PNPM_HOME /root/.pnpm
# Set environment
ARG ENVIRONMENT=production
# Add the pnpm global bin to the PATH
ENV PATH=/root/.pnpm/bin:$PATH
ENV PATH /root/.pnpm/bin:$PATH
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
COPY package.json pnpm-lock.yaml ./
COPY console/package.json console/pnpm-lock.yaml ./
# Copy over .npmrc if applicable
COPY .npmr[c] ./
COPY console/.npmr[c] ./
# Install global dependencies
RUN npm install -g ember-cli pnpm
@@ -33,7 +33,7 @@ RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN pnpm install
# Copy the console directory contents into the container at /console
COPY . .
COPY console .
# Build the application
RUN pnpm build --environment $ENVIRONMENT
@@ -48,7 +48,7 @@ COPY --from=builder /console/dist /usr/share/nginx/html
EXPOSE 4200
# Use custom nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY console/nginx.conf /etc/nginx/conf.d/default.conf
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,57 @@
# ---- Build Stage ----
FROM node:18.15.0-alpine
# Set the working directory in the container to /console
WORKDIR /console
# Create the pnpm directory and set the PNPM_HOME environment variable
RUN mkdir -p ~/.pnpm
ENV PNPM_HOME /root/.pnpm
# Set environment
ARG ENVIRONMENT=production
# Add the pnpm global bin to the PATH
ENV PATH /root/.pnpm/bin:$PATH
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
COPY console/package.json console/pnpm-lock.yaml ./
# Copy over .npmrc if applicable
COPY console/.npmr[c] ./
# Install global dependencies
RUN npm install -g ember-cli pnpm
# Install git
RUN apk update && apk add git openssh-client
# Trust GitHub's RSA host key
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
# Install app dependencies
RUN pnpm install
# Copy the console directory contents into the container at /console
COPY console .
# Build the application
RUN pnpm build --environment $ENVIRONMENT
# # Make sure the build output is available in /console/dist
# RUN ls -la /console/dist
# # ---- Serve Stage ----
# FROM nginx:alpine
# # Copy the built app to our served directory
# COPY --from=builder /console/dist /usr/share/nginx/html
# # Expose the port nginx is bound to
# EXPOSE 4201
# # Use custom nginx.conf
# COPY console/nginx.conf /etc/nginx/conf.d/default.conf
# # Start Nginx server
# CMD ["nginx", "-g", "daemon off;"]

View File

@@ -2,7 +2,8 @@ import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from '@fleetbase/console/config/environment';
import './deprecation-workflow';
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
import mapEngines from '@fleetbase/ember-core/utils/map-engines';
export default class App extends Application {
modulePrefix = config.modulePrefix;
@@ -10,6 +11,13 @@ export default class App extends Application {
Resolver = Resolver;
extensions = [];
engines = {};
async ready() {
const extensions = await loadExtensions();
this.extensions = extensions;
this.engines = mapEngines(extensions);
}
}
loadInitializers(App, config.modulePrefix);

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="Filesystem" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Filesystem" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Driver" @helpText="Select the default filesystem driver for Fleetbase to use.">
<Select @options={{this.disks}} @value={{this.driver}} @onSelect={{this.setDriver}} @placeholder="Select filesystem driver" class="w-full" disabled={{this.isLoading}} />
</InputGroup>

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="Mail" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Mail" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Mailer" @helpText="Select the default mailer for Fleetbase to use.">
<Select @options={{this.mailers}} @value={{this.mailer}} @onSelect={{this.setMailer}} @placeholder="Select mailer" class="w-full" />
</InputGroup>
@@ -13,14 +13,6 @@
<InputGroup @name="SMTP Timeout" @value={{this.smtpTimeout}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="SMTP Auth Mode" @value={{this.smtpAuth_mode}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
{{#if (eq this.mailer "microsoft-graph")}}
<InputGroup @name="Client ID" @value={{this.microsoftGraphClient_id}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Client Secret" @value={{this.microsoftGraphClient_secret}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Tenant ID" @value={{this.microsoftGraphTenant_id}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup>
<Toggle @isToggled={{this.microsoftGraphSave_to_sent_items}} @onToggle={{fn (mut this.microsoftGraphSave_to_sent_items)}} @label="Save to sent items" />
</InputGroup>
{{/if}}
{{#if (eq this.mailer "mailgun")}}
<InputGroup @name="Mailgun Domain" @value={{this.mailgunDomain}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Mailgun Endpoint" @value={{this.mailgunEndpoint}} disabled={{this.loadConfigValues.isRunning}} />

View File

@@ -26,10 +26,6 @@ export default class ConfigureMailComponent extends Component {
@tracked postmarkToken = null;
@tracked sendgridApi_key = null;
@tracked resendKey = null;
@tracked microsoftGraphClient_id = null;
@tracked microsoftGraphClient_secret = null;
@tracked microsoftGraphTenant_id = null;
@tracked microsoftGraphSave_to_sent_items = false;
/**
* Creates an instance of ConfigureFilesystemComponent.
@@ -68,19 +64,6 @@ export default class ConfigureMailComponent extends Component {
};
}
@action serializeMicrosoftGraphConfig() {
return {
client_id: this.microsoftGraphClient_id,
client_secret: this.microsoftGraphClient_secret,
tenant_id: this.microsoftGraphTenant_id,
save_to_sent_items: this.microsoftGraphSave_to_sent_items,
from: {
address: this.fromAddress,
name: this.fromName,
},
};
}
@action serializeMailgunConfig() {
return {
domain: this.mailgunDomain,
@@ -129,7 +112,6 @@ export default class ConfigureMailComponent extends Component {
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
microsoftGraph: this.serializeMicrosoftGraphConfig(),
});
} catch (error) {
this.notifications.serverError(error);
@@ -149,7 +131,6 @@ export default class ConfigureMailComponent extends Component {
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
microsoftGraph: this.serializeMicrosoftGraphConfig(),
});
this.notifications.success('Mail configuration saved.');
} catch (error) {

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="APN Configutation" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="APN Configutation" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="APN Key ID" @value={{this.apn.key_id}} disabled={{this.isLoading}} />
<InputGroup @name="APN Team ID" @value={{this.apn.team_id}} disabled={{this.isLoading}} />
<InputGroup @name="APN App Bundle ID" @value={{this.apn.app_bundle_id}} disabled={{this.isLoading}} />
@@ -20,7 +20,7 @@
</InputGroup>
</ContentPanel>
<ContentPanel @title="Firebase Configutation" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Firebase Configutation" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @wrapperClass="flex flex-row items-center mb-0i">
<UploadButton @name="firebase-service-account" @accept="text/plain,text/javascript,application/json" @onFileAdded={{this.uploadFirebaseCredentials}} @buttonText="Upload Service Account JSON" @icon="upload" class="w-auto m-0i mt-0i" />
{{#if this.firebase.credentials_file}}
@@ -33,7 +33,7 @@
</InputGroup>
</ContentPanel>
<ContentPanel @title="Test Push Notification" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Test Push Notification" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-900">
{{#if this.testResponse}}
<div class="flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="Queue" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Queue" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Driver" @helpText="Select the default queue driver for Fleetbase to use.">
<Select @options={{this.connections}} @value={{this.driver}} @onSelect={{this.setDriver}} @placeholder="Select queue driver" disabled={{this.isLoading}} class="w-full" />
</InputGroup>

View File

@@ -1,15 +1,15 @@
<ContentPanel @title="AWS" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="AWS" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="AWS Access Key" @value={{this.awsKey}} disabled={{this.isLoading}} />
<InputGroup @name="AWS Access Secret" @value={{this.awsSecret}} disabled={{this.isLoading}} />
<InputGroup @name="AWS Region" @value={{this.awsRegion}} disabled={{this.isLoading}} />
</ContentPanel>
<ContentPanel @title="Google Maps" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Google Maps" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Google Maps API Key" @value={{this.googleMapsApiKey}} disabled={{this.isLoading}} />
<InputGroup @name="Google Maps Locale" @value={{this.googleMapsLocale}} disabled={{this.isLoading}} />
</ContentPanel>
<ContentPanel @title="Twilio" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Twilio" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Twilio SID" @value={{this.twilioSid}} disabled={{this.isLoading}} />
<InputGroup @name="Twilio Token" @value={{this.twilioToken}} disabled={{this.isLoading}} />
<InputGroup @name="Twilio From" @value={{this.twilioFrom}} disabled={{this.isLoading}} />
@@ -25,7 +25,7 @@
</div>
</ContentPanel>
<ContentPanel @title="Sentry" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="Sentry" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Sentry DSN" @value={{this.sentryDsn}} disabled={{this.isLoading}} />
{{#if this.sentryTestResponse}}
<div class="flex flex-row items-center rounded-lg border {{if (eq this.sentryTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
@@ -36,7 +36,7 @@
<Button @wrapperClass="mt-3" @icon="plug" @text="Test Sentry Config" @onClick={{perform this.testSentry}} @isLoading={{this.testSentry.isRunning}} @disabled={{not this.sentryDsn}} />
</ContentPanel>
<ContentPanel @title="IP Info" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="IP Info" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="IP Info API Key" @value={{this.ipinfoApiKey}} disabled={{this.isLoading}} />
</ContentPanel>

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="SocketCluster Connection" @open={{true}} @wrapperClass="bordered-classic">
<ContentPanel @title="SocketCluster Connection" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<p class="mb-4">The SocketCluster configuration cannot be changed at this time.</p>
<div id="output" class="font-mono rounded-lg max-h-full px-6 py-4 overflow-y-scroll bg-black shadow-inner dark:shadow-none">
<div class="flex items-center justify-between mb-4">

View File

@@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { task } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
export default class MetricComponent extends Component {
@service fetch;

View File

@@ -0,0 +1,31 @@
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
<Overlay::Header @title={{t "component.dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
<div class="flex flex-1 justify-end">
<Button @type="default" @icon="times" @helpText={{t "component.dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
</div>
</Overlay::Header>
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-4 space-y-4 pt-4">
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
{{#each this.availableWidgets as |widget|}}
<div
class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
{{on "click" (fn this.addWidgetToDashboard widget)}}
>
<div class="flex flex-row items-center leading-6 mb-2.5">
<div class="w-8 flex items-center justify-start">
<FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
</div>
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
{{t "component.dashboard-widget-panel.widget-name" widgetName=widget.name}}
</p>
</div>
<div>
<p class="text-xs dark:text-gray-100 text-gray-800">{{widget.description}}</p>
</div>
</div>
{{/each}}
</div>
<Spacer @height="300px" />
</Overlay::Body>
</Overlay>

View File

@@ -0,0 +1,60 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class DashboardWidgetPanelComponent extends Component {
@service universe;
@tracked availableWidgets = [];
@tracked dashboard;
@tracked isOpen = true;
@service notifications;
/**
* Constructs the component and applies initial state.
*/
constructor(owner, { dashboard }) {
super(...arguments);
this.availableWidgets = this.universe.getDashboardWidgets();
this.dashboard = dashboard;
}
/**
* Sets the overlay context.
*
* @action
* @param {OverlayContextObject} overlayContext
*/
@action setOverlayContext(overlayContext) {
this.context = overlayContext;
if (typeof this.args.onLoad === 'function') {
this.args.onLoad(...arguments);
}
}
@action addWidgetToDashboard(widget) {
// If widget is a component definition/class
if (typeof widget.component === 'function') {
widget.component = widget.component.name;
}
this.args.dashboard.addWidget(widget).catch((error) => {
this.notifications.serverError(error);
});
}
/**
* Handles cancel button press.
*
* @action
*/
@action onPressClose() {
this.isOpen = false;
if (typeof this.args.onClose === 'function') {
this.args.onClose();
}
}
}

View File

@@ -8,7 +8,7 @@
</a>
</div>
<div class="px-4 py-2.5">
{{#if this.loadBlogPosts.isRunning}}
{{#if this.isLoading}}
<Spinner />
{{else}}
<ul class="space-y-2">

View File

@@ -1,42 +1,28 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { isArray } from '@ember/array';
import { storageFor } from 'ember-local-storage';
import { add, isPast } from 'date-fns';
import { task } from 'ember-concurrency';
import { action } from '@ember/object';
export default class FleetbaseBlogComponent extends Component {
@storageFor('local-cache') localCache;
@service fetch;
@tracked posts = [];
@tracked isLoading = false;
constructor() {
super(...arguments);
this.loadBlogPosts.perform();
this.loadBlogPosts();
}
@task *loadBlogPosts() {
// Check if cached data and expiration are available
const cachedData = this.localCache.get('fleetbase-blog-data');
const expiration = this.localCache.get('fleetbase-blog-data-expiration');
@action loadBlogPosts() {
this.isLoading = true;
// Check if the cached data is still valid
if (cachedData && isArray(cachedData) && expiration && !isPast(new Date(expiration))) {
// Use cached data
this.posts = cachedData;
} else {
// Fetch new data
try {
const data = yield this.fetch.get('lookup/fleetbase-blog');
this.posts = isArray(data) ? data : [];
if (data) {
this.localCache.set('fleetbase-blog-data', data);
this.localCache.set('fleetbase-blog-data-expiration', add(new Date(), { hours: 6 }));
}
} catch (err) {
debug('Failed to load blog: ' + err.message);
}
}
return this.fetch
.get('lookup/fleetbase-blog')
.then((response) => {
this.posts = response;
})
.finally(() => {
this.isLoading = false;
});
}
}

View File

@@ -52,7 +52,7 @@ export default class GithubCardComponent extends Component {
this.data = cachedData;
} else {
// Fetch new data
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase', { cache: 'default' });
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase');
if (response.ok) {
this.data = yield response.json();
this.localCache.set('fleetbase-github-data', this.data);
@@ -72,7 +72,7 @@ export default class GithubCardComponent extends Component {
this.tags = cachedTags;
} else {
// Fetch new tags
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags', { cache: 'default' });
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags');
if (response.ok) {
this.tags = yield response.json();
this.localCache.set('fleetbase-github-tags', this.tags);

View File

@@ -1,61 +0,0 @@
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
<div class="mt-2">
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
</div>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
<form {{on "submit" (perform this.onboard)}}>
{{#if this.error}}
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
{{/if}}
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup
@name={{t "onboard.index.confirm-password"}}
@value={{this.password_confirmation}}
@type="password"
@helpText={{t "onboard.index.confirm-password-help-text"}}
@inputClass="input-lg"
/>
<div class="flex items-center justify-end mt-5">
<Button
@buttonType="submit"
@icon="check"
@iconPrefix="fas"
@type="primary"
@size="lg"
@text={{t "onboard.index.continue-button-text"}}
@isLoading={{this.onboard.isRunning}}
@disabled={{not this.filled}}
/>
</div>
</form>
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
<YieldedComponent @context={{ctx}} />
</RegistryYield>
</div>
</div>
</div>

View File

@@ -1,77 +0,0 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, getProperties } from '@ember/object';
import { isBlank } from '@ember/utils';
import { task } from 'ember-concurrency';
import OnboardValidations from '../../validations/onboard';
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
export default class OnboardingFormComponent extends Component {
@service fetch;
@service session;
@service router;
@service notifications;
@service urlSearchParams;
@tracked name;
@tracked email;
@tracked phone;
@tracked organization_name;
@tracked password;
@tracked password_confirmation;
@tracked error;
get filled() {
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
return Object.values(input).every((val) => !isBlank(val));
}
@task *onboard(event) {
event?.preventDefault?.();
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
yield changeset.validate();
if (changeset.get('isInvalid')) {
const errorMessage = changeset.errors.firstObject.validation.firstObject;
this.notifications.error(errorMessage);
return;
}
// Set user timezone
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
try {
const { status, skipVerification, token, session } = yield this.fetch.post('onboard/create-account', input);
if (status !== 'success') {
this.notifications.error('Onboard failed');
return;
}
// save session
this.args.context.persist('session', session);
if (skipVerification === true && token) {
// only manually authenticate if skip verification
this.session.isOnboarding().manuallyAuthenticate(token);
yield this.router.transitionTo('console');
return this.notifications.success('Welcome to Fleetbase!');
} else {
this.args.orchestrator.next();
this.urlSearchParams.setParamsToCurrentUrl({
step: this.args.orchestrator?.current?.id,
session,
});
}
} catch (err) {
this.notifications.serverError(err);
}
}
}

View File

@@ -1,82 +0,0 @@
{{page-title (t "onboard.verify-email.header-title")}}
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
{{#if this.initialized}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.verify-email.title"}}
</h2>
</div>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</InfoBlock>
<form class="mt-8 space-y-6" {{on "submit" (perform this.verify)}}>
<InputGroup
@type="tel"
@name={{t "onboard.verify-email.verification-input-label"}}
@value={{this.code}}
@helpText={{t "onboard.verify-email.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.verification.validateInput}}
{{did-insert this.verification.validateInput}}
/>
<div class="flex flex-row items-center space-x-4">
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.verify.isRunning}}
@disabled={{not this.verification.ready}}
/>
<a href="#" {{on "click" this.verification.didntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
</div>
{{#if this.verification.waiting}}
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendBySms}}
/>
</div>
</div>
{{/if}}
</form>
</div>
{{/if}}
</div>
</div>

View File

@@ -1,53 +0,0 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later, next } from '@ember/runloop';
import { not } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default class OnboardingVerifyEmailComponent extends Component {
@service('session') authSession;
@service('user-verification') verification;
@service fetch;
@service notifications;
@service router;
@service urlSearchParams;
@tracked code;
@tracked session;
@tracked initialized = false;
constructor() {
super(...arguments);
next(() => this.#initialize());
}
#initialize() {
this.code = this.urlSearchParams.get('code');
this.session = this.args.context.get('session') ?? this.urlSearchParams.get('session');
this.initialized = true;
this.verification.start();
}
@task *verify(event) {
event?.preventDefault?.();
try {
const { status, token } = yield this.fetch.post('onboard/verify-email', { session: this.session, code: this.code });
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.authSession.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
}
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -1,13 +0,0 @@
<section class="onboarding step-host">
{{#if this.initialized}}
{{#if this.orchestrator.wrapper}}
{{component (lazy-engine-component this.orchestrator.wrapper) currentStepComponent=this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{else if this.currentComponent}}
{{component (lazy-engine-component this.currentComponent) context=this.context orchestrator=this.orchestrator brand=@brand}}
{{/if}}
{{else}}
<div class="flex items-center justify-center min-h-24">
<Spinner />
</div>
{{/if}}
</section>

View File

@@ -1,27 +0,0 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { next } from '@ember/runloop';
export default class OnboardingYieldComponent extends Component {
@service('onboarding-orchestrator') orchestrator;
@service('onboarding-context') context;
@tracked initialized = false;
get currentComponent() {
return this.orchestrator.current && this.orchestrator.current.component;
}
constructor(owner, { step, session, code }) {
super(...arguments);
next(() => this.#initialize(step, session, code));
}
#initialize(step, session, code) {
if (step) this.orchestrator.goto(step);
if (session) this.context.persist('session', session);
if (code) this.context.set('code', code);
this.initialized = true;
}
}

View File

@@ -1,6 +1,6 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';

View File

@@ -4,7 +4,6 @@ import { inject as service } from '@ember/service';
import { later } from '@ember/runloop';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { dasherize } from '@ember/string';
import first from '@fleetbase/ember-core/utils/first';
export default class ConsoleController extends Controller {
@@ -17,19 +16,67 @@ export default class ConsoleController extends Controller {
@service intl;
@service universe;
@service abilities;
@service sidebar;
/**
* Authenticated user organizations.
*
* @var {Array}
*/
@tracked organizations = [];
/**
* Sidebar Context Controls
*
* @var {SidebarContext}
*/
@tracked sidebarContext;
/**
* State of sidebar toggle icon
*
* @var {SidebarContext}
*/
@tracked sidebarToggleEnabled = true;
/**
* The sidebar toggle state.
*
* @var {SidebarContext}
*/
@tracked sidebarToggleState = {};
/**
* Routes which should hide the sidebar menu.
*
* @var {Array}
*/
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications', 'console.virtual'];
/**
* Menu items to be added to the main header navigation bar.
*
* @memberof ConsoleController
*/
@tracked menuItems = [];
/**
* Menu items to be added to the user dropdown menu located in the header.
*
* @memberof ConsoleController
*/
@tracked userMenuItems = [];
/**
* Menu items to be added to the organization dropdown menu located in the header.
*
* @memberof ConsoleController
*/
@tracked organizationMenuItems = [];
get currentRouteClass() {
return dasherize(this.router.currentRouteName.replace(/\./g, ' '));
}
/**
* Creates an instance of ConsoleController.
* @memberof ConsoleController
*/
constructor() {
super(...arguments);
this.router.on('routeDidChange', (transition) => {
@@ -42,17 +89,17 @@ export default class ConsoleController extends Controller {
// Hide the sidebar if the current route is in hiddenSidebarRoutes
if (shouldHideSidebar) {
this.sidebar.hideNow();
this.sidebarContext.hideNow();
this.sidebarToggleEnabled = false;
return; // Exit early as no further action is required
}
// If the sidebar was manually closed and not on a hidden route, keep it closed
if (isSidebarManuallyClosed) {
this.sidebar.hideNow();
this.sidebarContext.hideNow();
} else {
// Otherwise, show the sidebar
this.sidebar.show();
this.sidebarContext.show();
}
// Ensure toggle is enabled unless on a hidden route
@@ -87,7 +134,7 @@ export default class ConsoleController extends Controller {
this.universe.trigger('sidebarContext.available', sidebarContext);
if (this.hiddenSidebarRoutes.includes(this.router.currentRouteName)) {
this.sidebar.hideNow();
this.sidebarContext.hideNow();
this.sidebarToggleEnabled = false;
}
}

View File

@@ -2,6 +2,10 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ConsoleAccountController extends Controller {
@service('universe/menu-service') menuService;
/**
* Inject the `universe` service.
*
* @memberof ConsoleAdminController
*/
@service universe;
}

View File

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
/**

View File

@@ -1,9 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';
export default class ConsoleAccountIndexController extends Controller {
@@ -42,18 +40,6 @@ export default class ConsoleAccountIndexController extends Controller {
*/
@alias('currentUser.user') user;
/**
* Available timezones for selection.
*
* @memberof ConsoleAccountIndexController
*/
@tracked timezones = [];
constructor() {
super(...arguments);
this.loadTimezones.perform();
}
/**
* Handle upload of new photo
*
@@ -68,7 +54,6 @@ export default class ConsoleAccountIndexController extends Controller {
subject_uuid: this.user.id,
subject_type: 'user',
type: 'user_avatar',
resize: 'md'
},
(uploadedFile) => {
this.user.setProperties({
@@ -131,19 +116,6 @@ export default class ConsoleAccountIndexController extends Controller {
return isPasswordValid;
}
/**
* Load all available timezones from lookup.
*
* @memberof ConsoleAccountIndexController
*/
@task *loadTimezones() {
try {
this.timezones = yield this.fetch.get('lookup/timezones');
} catch (error) {
debug(`Unable to load timezones : ${error.message}`);
}
}
/**
* Checks if any user attribute has been changed
*

View File

@@ -2,6 +2,10 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ConsoleAdminController extends Controller {
@service('universe/menu-service') menuService;
/**
* Inject the `universe` service.
*
* @memberof ConsoleAdminController
*/
@service universe;
}

View File

@@ -0,0 +1,136 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import createNotificationKey from '../../../utils/create-notification-key';
export default class ConsoleAdminNotificationsController extends Controller {
/**
* Inject the notifications service.
*
* @memberof ConsoleAdminNotificationsController
*/
@service notifications;
/**
* Inject the fetch service.
*
* @memberof ConsoleAdminNotificationsController
*/
@service fetch;
/**
* The notification settings value JSON.
*
* @memberof ConsoleAdminNotificationsController
* @var {Object}
*/
@tracked notificationSettings = {};
/**
* Notification transport methods enabled.
*
* @memberof ConsoleAdminNotificationsController
* @var {Array}
*/
@tracked notificationTransportMethods = ['email', 'sms'];
/**
* Tracked property for the loading state
*
* @memberof ConsoleAdminNotificationsController
* @var {Boolean}
*/
@tracked isLoading = false;
/**
* Creates an instance of ConsoleAdminNotificationsController.
* @memberof ConsoleAdminNotificationsController
*/
constructor() {
super(...arguments);
this.getSettings();
}
/**
* Selectes notifiables for settings.
*
* @param {Object} notification
* @param {Array} notifiables
* @memberof ConsoleAdminNotificationsController
*/
@action onSelectNotifiable(notification, notifiables) {
const notificationKey = createNotificationKey(notification.definition, notification.name);
const _notificationSettings = { ...this.notificationSettings };
if (!_notificationSettings[notificationKey]) {
_notificationSettings[notificationKey] = {};
}
_notificationSettings[notificationKey].notifiables = notifiables;
_notificationSettings[notificationKey].definition = notification.definition;
_notificationSettings[notificationKey].via = notifiables.map((notifiable) => {
return {
identifier: notifiable.value,
methods: this.notificationTransportMethods,
};
});
this.mutateNotificationSettings(_notificationSettings);
}
/**
* Mutates the notification settings property.
*
* @param {Object} [_notificationSettings={}]
* @memberof ConsoleAdminNotificationsController
*/
mutateNotificationSettings(_notificationSettings = {}) {
this.notificationSettings = {
...this.notificationSettings,
..._notificationSettings,
};
}
/**
* Save notification settings to the server.
*
* @action
* @method saveSettings
* @returns {Promise}
* @memberof ConsoleAdminNotificationsController
*/
@action saveSettings() {
const { notificationSettings } = this;
this.isLoading = true;
return this.fetch
.post('notifications/save-settings', { notificationSettings })
.then(() => {
this.notifications.success('Notification settings successfully saved.');
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
/**
* Fetches and updates notification settings asynchronously.
*
* @returns {Promise<void>} A promise for successful retrieval and update, or an error on failure.
*/
getSettings() {
return this.fetch
.get('notifications/get-settings')
.then(({ notificationSettings }) => {
this.notificationSettings = notificationSettings;
})
.catch((error) => {
this.notifications.serverError(error);
});
}
}

View File

@@ -77,7 +77,7 @@ export default class ConsoleAdminOrganizationsIndexUsersController extends Contr
valuePath: 'roleName',
},
{
label: this.intl.t('common.phone'),
label: this.intl.t('common.phone-number'),
valuePath: 'phone',
},
{

View File

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
/**

View File

@@ -2,9 +2,6 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleAdminVirtualController extends Controller {
@tracked bodyClass = 'overflow-y-scroll h-full';
@tracked containerClass = 'container mx-auto h-screen';
@tracked wrapperClass = 'max-w-3xl my-10 mx-auto';
@tracked view;
queryParams = ['view'];
}

View File

@@ -2,6 +2,10 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ConsoleSettingsController extends Controller {
@service('universe/menu-service') menuService;
/**
* INject the `universe` service
*
* @memberof ConsoleSettingsController
*/
@service universe;
}

View File

@@ -2,8 +2,6 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';
export default class ConsoleSettingsIndexController extends Controller {
/**
@@ -27,6 +25,13 @@ export default class ConsoleSettingsIndexController extends Controller {
*/
@service fetch;
/**
* The request loading state.
*
* @memberof ConsoleSettingsIndexController
*/
@tracked isLoading = false;
/**
* the upload queue.
*
@@ -41,32 +46,23 @@ export default class ConsoleSettingsIndexController extends Controller {
*/
@tracked uploadedFiles = [];
/**
* Available timezones for selection.
*
* @memberof ConsoleAccountIndexController
*/
@tracked timezones = [];
constructor() {
super(...arguments);
this.loadTimezones.perform();
}
/**
* Save the organization settings.
*
* @memberof ConsoleSettingsIndexController
*/
@task *saveSettings(event) {
event?.preventDefault();
@action saveSettings(event) {
event.preventDefault();
this.isLoading = true;
try {
yield this.model.save();
this.notifications.success('Organization changes successfully saved.');
} catch (error) {
debug(`Unable to save organization settings : ${error.message}`);
}
this.model
.save()
.then(() => {
this.notifications.success('Organization changes successfully saved.');
})
.finally(() => {
this.isLoading = false;
});
}
/**
@@ -95,17 +91,4 @@ export default class ConsoleSettingsIndexController extends Controller {
}
);
}
/**
* Load all available timezones from lookup.
*
* @memberof ConsoleAccountIndexController
*/
@task *loadTimezones() {
try {
this.timezones = yield this.fetch.get('lookup/timezones');
} catch (error) {
debug(`Unable to load timezones : ${error.message}`);
}
}
}

View File

@@ -1,152 +0,0 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import createNotificationKey from '@fleetbase/ember-core/utils/create-notification-key';
import { task } from 'ember-concurrency';
export default class ConsoleSettingsNotificationsController extends Controller {
@service notifications;
@service fetch;
@service store;
@service currentUser;
@tracked notificationSettings = {};
@tracked notificationTransportMethods = ['email', 'sms'];
@tracked company;
/**
* Creates an instance of ConsoleSettingsNotificationsController.
* @memberof ConsoleSettingsNotificationsController
*/
constructor() {
super(...arguments);
this.getSettings.perform();
}
/**
* Toggles the "Alphanumeric Sender ID" feature for the current company.
*
* Updates the company's `options` object by setting the
* `alpha_numeric_sender_id_enabled` flag. This controls whether the
* organization uses a custom alphanumeric sender ID when sending SMS.
*
* @action
* @param {boolean} enabled - Whether the feature should be enabled or disabled.
* @returns {void}
*/
@action toggleAlphaNumericSenderId(enabled) {
const currentOptions = this.company.options ?? {};
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id_enabled: enabled });
}
/**
* Sets the Alphanumeric Sender ID string for the current company.
*
* Reads the input's value from the event and updates the company's `options`
* object by setting the `alpha_numeric_sender_id` field. This value represents
* the sender name that will appear in outbound SMS messages (subject to carrier
* support and restrictions).
*
* @action
* @param {Event} event - Input event containing the alphanumeric sender ID value.
* @returns {void}
*/
@action setAlphaNumericSenderId(event) {
const value = event.target.value;
const currentOptions = this.company.options ?? {};
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id: value });
}
/**
* Selectes notifiables for settings.
*
* @param {Object} notification
* @param {Array} notifiables
* @memberof ConsoleSettingsNotificationsController
*/
@action onSelectNotifiable(notification, notifiables) {
const notificationKey = createNotificationKey(notification.definition, notification.name);
const _notificationSettings = { ...this.notificationSettings };
if (!_notificationSettings[notificationKey]) {
_notificationSettings[notificationKey] = {};
}
_notificationSettings[notificationKey].notifiables = notifiables;
_notificationSettings[notificationKey].definition = notification.definition;
_notificationSettings[notificationKey].via = notifiables.map((notifiable) => {
return {
identifier: notifiable.value,
methods: this.notificationTransportMethods,
};
});
this.mutateNotificationSettings(_notificationSettings);
}
/**
* Mutates the notification settings property.
*
* @param {Object} [_notificationSettings={}]
* @memberof ConsoleSettingsNotificationsController
*/
mutateNotificationSettings(_notificationSettings = {}) {
this.notificationSettings = {
...this.notificationSettings,
..._notificationSettings,
};
}
/**
* Save notification settings.
*
* @memberof ConsoleSettingsNotificationsController
*/
@task *saveSettings() {
const { notificationSettings } = this;
try {
yield this.fetch.post('notifications/save-settings', { notificationSettings: notificationSettings ?? {} });
yield this.saveCompanyOptions.perform();
this.notifications.success('Notification settings successfully saved.');
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Get notification settings.
*
* @memberof ConsoleSettingsNotificationsController
*/
@task *getSettings() {
try {
const { notificationSettings } = yield this.fetch.get('notifications/get-settings');
this.notificationSettings = notificationSettings;
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Saves the updated company options to the backend.
*
* This ember-concurrency task attempts to persist the company's modified
* `options` object by calling `company.save()`. If the request fails, a server
* error notification is displayed. No action is taken if no company is loaded.
*
* @task
* @generator
* @yields {Promise} Resolves when the save request completes.
* @returns {Promise<void>} Task completion state.
*/
@task *saveCompanyOptions() {
if (!this.company) return;
try {
yield this.company.save();
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
export default class ConsoleSettingsTwoFaController extends Controller {

View File

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
export default class InstallController extends Controller {
@service fetch;

View File

@@ -1,8 +1,151 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, getProperties } from '@ember/object';
import OnboardValidations from '../../validations/onboard';
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
export default class OnboardIndexController extends Controller {
@tracked step;
@tracked session;
@tracked code;
/**
* Inject the `fetch` service
*
* @memberof OnboardIndexController
*/
@service fetch;
/**
* Inject the `session` service
*
* @memberof OnboardIndexController
*/
@service session;
/**
* Inject the `router` service
*
* @memberof OnboardIndexController
*/
@service router;
/**
* Inject the `notifications` service
*
* @memberof OnboardIndexController
*/
@service notifications;
/**
* The name input field.
*
* @memberof OnboardIndexController
*/
@tracked name;
/**
* The email input field.
*
* @memberof OnboardIndexController
*/
@tracked email;
/**
* The phone input field.
*
* @memberof OnboardIndexController
*/
@tracked phone;
/**
* The organization_name input field.
*
* @memberof OnboardIndexController
*/
@tracked organization_name;
/**
* The password input field.
*
* @memberof OnboardIndexController
*/
@tracked password;
/**
* The name password confirmation field.
*
* @memberof OnboardIndexController
*/
@tracked password_confirmation;
/**
* The property for error message.
*
* @memberof OnboardIndexController
*/
@tracked error;
/**
* The loading state of the onboard request.
*
* @memberof OnboardIndexController
*/
@tracked isLoading = false;
/**
* The ready state for the form.
*
* @memberof OnboardIndexController
*/
@tracked readyToSubmit = false;
/**
* Start the onboard process.
*
* @return {Promise}
* @memberof OnboardIndexController
*/
@action async startOnboard(event) {
event.preventDefault();
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
await changeset.validate();
if (changeset.get('isInvalid')) {
const errorMessage = changeset.errors.firstObject.validation.firstObject;
this.notifications.error(errorMessage);
return;
}
// Set user timezone
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
this.isLoading = true;
return this.fetch
.post('onboard/create-account', input)
.then(({ status, skipVerification, token, session }) => {
if (status === 'success') {
if (skipVerification === true && token) {
// only manually authenticate if skip verification
this.session.isOnboarding().manuallyAuthenticate(token);
return this.router.transitionTo('console').then(() => {
this.notifications.success('Welcome to Fleetbase!');
});
}
return this.router.transitionTo('onboard.verify-email', { queryParams: { hello: session } });
}
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
}

View File

@@ -1,7 +0,0 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class VirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -1,9 +0,0 @@
import setupDeprecationWorkflow from 'ember-cli-deprecation-workflow';
setupDeprecationWorkflow({
workflow: [
{ handler: 'silence', matchId: 'ember-concurrency.deprecate-decorator-task' },
{ handler: 'silence', matchId: 'new-helper-names' },
{ handler: 'silence', matchId: 'ember-data:deprecate-non-strict-relationships' },
],
});

View File

@@ -0,0 +1,6 @@
import { helper } from '@ember/component/helper';
import createNotificationKey from '../utils/create-notification-key';
export default helper(function getNotificationKey([definition, name]) {
return createNotificationKey(definition, name);
});

View File

@@ -10,11 +10,9 @@
{{content-for "head"}}
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="256x256" href="/favicon/android-chrome-256x256.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">

View File

@@ -1,47 +0,0 @@
import translations from 'ember-intl/translations';
import { all } from 'rsvp';
const isBrowser = typeof window !== 'undefined';
const isValidLang = (lang) => typeof lang === 'string' && /^[a-z]{2,3}$/i.test(lang);
function langOf(tag = 'en') {
return String(tag).toLowerCase().split('-')[0];
}
async function loadBasePolyfills() {
await import('@formatjs/intl-numberformat/polyfill-force');
await import('@formatjs/intl-pluralrules/polyfill-force');
await import('@formatjs/intl-datetimeformat/polyfill-force');
await import('@formatjs/intl-relativetimeformat/polyfill-force');
}
async function loadLocaleData(lang) {
if (!isValidLang(lang)) return;
return all([
import(`@formatjs/intl-numberformat/locale-data/${lang}.js`),
import(`@formatjs/intl-pluralrules/locale-data/${lang}.js`),
import(`@formatjs/intl-datetimeformat/locale-data/${lang}.js`),
import(`@formatjs/intl-relativetimeformat/locale-data/${lang}.js`),
]);
}
export function initialize(application) {
if (!isBrowser) return;
const locales = translations.map(([locale]) => String(locale));
const langs = [...new Set(locales.map(langOf).filter(isValidLang))];
application.deferReadiness();
(async () => {
await loadBasePolyfills();
await all(langs.map(loadLocaleData));
application.advanceReadiness();
})();
}
export default {
name: 'load-intl-polyfills',
initialize,
};

View File

@@ -1,41 +0,0 @@
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
import { debug } from '@ember/debug';
/**
* Load Runtime Config Initializer
*
* Loads runtime configuration from fleetbase.config.json before the application boots.
* This must run first to ensure all config is available for other initializers.
*
* Uses `before` to ensure it runs before any other initializers.
*
* @export
* @param {Application} application
*/
export function initialize(application) {
const startTime = performance.now();
debug('[Runtime Config] Loading runtime configuration...');
// Defer readiness until config is loaded
application.deferReadiness();
(async () => {
try {
await loadRuntimeConfig();
const endTime = performance.now();
debug(`[Runtime Config] Runtime config loaded in ${(endTime - startTime).toFixed(2)}ms`);
application.advanceReadiness();
} catch (error) {
console.error('[Runtime Config] Failed to load runtime config:', error);
// Still advance readiness to prevent hanging
application.advanceReadiness();
}
})();
}
export default {
name: 'load-runtime-config',
initialize,
// Run after intl polyfills are loaded, before socketcluster
after: 'load-intl-polyfills',
before: 'load-socketcluster-client',
};

View File

@@ -1,38 +0,0 @@
import applyRouterFix from '@fleetbase/console/utils/router-refresh-patch';
import { debug } from '@ember/debug';
/**
* Apply Router Fix Instance Initializer
*
* Applies the Fleetbase router refresh bug fix patch.
* This patches the Ember router to handle dynamic segments correctly
* when refreshing routes with query parameters.
*
* Runs as an instance-initializer because it needs access to the
* application instance and router service.
*
* Bug: https://github.com/emberjs/ember.js/issues/19260
*
* @export
* @param {ApplicationInstance} appInstance
*/
export function initialize(appInstance) {
const startTime = performance.now();
debug('[Initializing Router Patch] Applying router refresh bug fix...');
try {
applyRouterFix(appInstance);
const endTime = performance.now();
debug(`[Initializing Router Patch] Router fix applied in ${(endTime - startTime).toFixed(2)}ms`);
} catch (error) {
console.error('[Initializing Router Patch] Failed to apply router fix:', error);
}
}
export default {
name: 'apply-router-fix',
initialize,
// Run before extension loading to ensure router is patched early
before: 'load-extensions',
};

View File

@@ -1,20 +0,0 @@
import { debug } from '@ember/debug';
/**
* Create console-specific registries
* Runs after extensions are loaded
*/
export function initialize(appInstance) {
const registryService = appInstance.lookup('service:universe/registry-service');
debug('[Initializing Registries] Creating console registries...');
// Create console-specific registries
registryService.createRegistries(['@fleetbase/console', 'auth:login']);
}
export default {
name: 'initialize-registries',
after: 'load-extensions',
initialize,
};

View File

@@ -1,47 +1,36 @@
import { Widget } from '@fleetbase/ember-core/contracts';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { debug } from '@ember/debug';
/**
* Register dashboard and widgets for FleetbaseConsole
* Runs after extensions are loaded
*/
export function initialize(appInstance) {
const widgetService = appInstance.lookup('service:universe/widget-service');
debug('[Initializing Widgets] Registering console dashboard and widgets...');
// Register the console dashboard
widgetService.registerDashboard('dashboard');
// Create widget definitions
const widgets = [
new Widget({
id: 'fleetbase-blog',
export function initialize(application) {
const universe = application.lookup('service:universe');
const defaultWidgets = [
{
widgetId: 'fleetbase-blog',
name: 'Fleetbase Blog',
description: 'Lists latest news and events from the Fleetbase official team.',
icon: 'newspaper',
component: 'fleetbase-blog',
grid_options: { w: 8, h: 9, minW: 8, minH: 9 },
default: true,
}),
new Widget({
id: 'fleetbase-github-card',
options: {
title: 'Fleetbase Blog',
},
},
{
widgetId: 'fleetbase-github-card',
name: 'Github Card',
description: 'Displays current Github stats from the official Fleetbase repo.',
icon: faGithub,
component: 'github-card',
grid_options: { w: 4, h: 8, minW: 4, minH: 8 },
default: true,
}),
options: {
title: 'Github Card',
},
},
];
// Register widgets
widgetService.registerWidgets('dashboard', widgets);
universe.registerDefaultDashboardWidgets(defaultWidgets);
universe.registerDashboardWidgets(defaultWidgets);
}
export default {
name: 'initialize-widgets',
after: 'load-extensions',
initialize,
};

View File

@@ -1,19 +1,11 @@
/**
* Load extensions from the API using ExtensionManager
* This must run before other initializers that depend on extensions
*/
export async function initialize(appInstance) {
const application = appInstance.application;
const extensionManager = appInstance.lookup('service:universe/extension-manager');
try {
await extensionManager.loadExtensions(application);
} catch (error) {
console.error('[load-extensions] Error:', error);
export function initialize(application) {
const universe = application.lookup('service:universe');
if (universe) {
universe.createRegistries(['@fleetbase/console', 'auth:login']);
universe.bootEngines(application);
}
}
export default {
name: 'load-extensions',
initialize,
};

View File

@@ -14,6 +14,5 @@ export function initialize(application) {
}
export default {
name: 'load-leaflet',
initialize,
};

View File

@@ -1,19 +0,0 @@
export function initialize(owner) {
const registry = owner.lookup('service:onboarding-registry');
if (registry) {
const defaultFlow = {
id: 'default@v1',
entry: 'signup',
steps: [
{ id: 'signup', component: 'onboarding/form', next: 'verify-email' },
{ id: 'verify-email', component: 'onboarding/verify-email' },
],
};
registry.registerFlow(defaultFlow);
}
}
export default {
initialize,
};

View File

@@ -1,11 +0,0 @@
export function initialize(appInstance) {
// Look up UniverseService and set the application instance
const universeService = appInstance.lookup('service:universe');
if (universeService) {
universeService.setApplicationInstance(appInstance);
}
}
export default {
initialize
};

View File

@@ -1,16 +0,0 @@
/**
* Setup extensions by loading and executing their extension.js files
* Runs after extensions are loaded from API
*/
export async function initialize(appInstance) {
const universe = appInstance.lookup('service:universe');
const extensionManager = appInstance.lookup('service:universe/extension-manager');
await extensionManager.setupExtensions(appInstance, universe);
}
export default {
name: 'setup-extensions',
after: ['load-extensions', 'initialize-registries', 'initialize-widgets'],
initialize,
};

View File

@@ -1,20 +0,0 @@
import Model, { attr } from '@ember-data/model';
export default class ActivityModel extends Model {
@attr('string') uuid;
@attr('string') log_name;
@attr('string') description;
@attr('string') company_id;
@attr('string') subject_id;
@attr('string') subject_type;
@attr('string') humanized_subject_type;
@attr('string') event;
@attr('string') causer_id;
@attr('string') causer_type;
@attr('string') humanized_causer_type;
@attr('object') properties;
@attr('object') causer;
@attr('object') subject;
@attr('date') created_at;
@attr('date') updated_at;
}

Some files were not shown because too many files have changed in this diff Show More