Compare commits

..

103 Commits

Author SHA1 Message Date
Ron
f52c1a17b8 Merge pull request #191 from fleetbase/dev-v0.3.9
v0.3.9
2024-01-25 20:04:33 +08:00
Ronald A. Richardson
c44e9186d5 ready for release 2024-01-25 20:04:01 +08:00
Ronald A. Richardson
02408b3a12 Merge branch 'dev-v0.3.10' of github.com:fleetbase/fleetbase into dev-v0.3.10 2024-01-25 19:38:55 +08:00
Ronald A. Richardson
a3d3cb05cf preparing for next release 2024-01-25 19:38:20 +08:00
Ron
604365fc5f Merge pull request #188 from ekini/feature/helm_services
Add scheduler and events to deployments
2024-01-25 19:12:39 +08:00
Ron
5d47dc676f Merge pull request #190 from fleetbase/fix-onboard-form
Fix onboard form
2024-01-25 19:11:58 +08:00
Ronald A. Richardson
50b58a5955 preparing next release 2024-01-25 19:10:17 +08:00
Munkh-erdene Damdinbazar
948db173c9 fixed verify email 2024-01-25 15:53:27 +08:00
Munkh-erdene Damdinbazar
4f56e04166 fixed onboard translations 2024-01-25 15:25:32 +08:00
Eugene Dementyev
d9705bf4f4 Add scheduler and events to deployments 2024-01-25 20:05:42 +13:00
Ron
5535067d87 Merge pull request #186 from fleetbase/dev-v0.3.8
v0.3.8
2024-01-24 18:25:53 +08:00
Ronald A. Richardson
aab4e9f1f6 remove incomplete mongolian translations 2024-01-24 18:22:24 +08:00
Ronald A. Richardson
43329a0ed2 bump package verison v0.3.8 2024-01-24 18:15:10 +08:00
Ronald A. Richardson
427816d2dd fix submodules, update dependency lockfiles 2024-01-24 18:14:48 +08:00
Ronald A. Richardson
3d9d5cbe9d Merge branch 'dev-v0.3.8' of github.com:fleetbase/fleetbase into dev-v0.3.8 2024-01-24 15:54:25 +08:00
Ronald A. Richardson
3fc5bdb32e hotfix organization 2fa settings as should not display method selection 2024-01-24 15:53:18 +08:00
Munkh-erdene Damdinbazar
a60f75655d Merge branch 'dev-v0.3.8' of github.com:fleetbase/fleetbase into dev-v0.3.8 2024-01-24 14:46:46 +08:00
Munkh-erdene Damdinbazar
0984af400a fixed some typo errors on templates and translation file 2024-01-24 14:45:21 +08:00
Ronald A. Richardson
7f900fe218 now using fleetbase intl lint module 2024-01-24 14:31:17 +08:00
Ronald A. Richardson
3aaf382384 added intl linter for console 2024-01-24 11:46:16 +08:00
Ronald A. Richardson
f589b1cf0a added intl linter script to console 2024-01-24 11:44:26 +08:00
Ronald A. Richardson
bce1cbf22d increase veirification timeout before recommending resend 2024-01-24 11:42:28 +08:00
Ron
6327603857 Merge pull request #187 from fleetbase/hotfix-s3deploy-dockerfile
Hotfix s3deploy dockerfile
2024-01-24 11:09:55 +08:00
Ronald A. Richardson
7204ddb3a9 Upgrade the console dockerfile to use node 18, patch the .s3deploy file to set correct cache-control header for json and xml files 2024-01-24 11:07:32 +08:00
Munkh-erdene Damdinbazar
2bc9745b95 add missing translation 2024-01-23 11:51:46 +08:00
Ron
4a2b3e094f Merge pull request #178 from fleetbase/feature-intl
feature: Internationalization
2024-01-22 18:51:23 +08:00
Ronald A. Richardson
313906d36a added locale switcher to change language of fleetbase 2024-01-22 18:50:09 +08:00
Ronald A. Richardson
d1d5b87c21 merged with latest changes for next release 2024-01-22 17:17:31 +08:00
Ronald A. Richardson
8dc7d9ff87 Improved the app styling of enforcement alert 2024-01-22 17:00:28 +08:00
TemuulenBM
008faa1ca7 corrected based on request 2024-01-22 16:20:13 +08:00
TemuulenBM
6aab30b358 remove all ds store files 2024-01-22 15:26:36 +08:00
Temuulen
30ab3aab09 Merge pull request #170 from fleetbase/feature-2FA
feature: 2FA
2024-01-22 14:54:41 +08:00
TemuulenBM
df3c93803d merged 2fa with latest release 2024-01-22 14:52:56 +08:00
TemuulenBM
3d1e20439f fixed styling for two-fa enforcement alert 2024-01-22 14:46:38 +08:00
TemuulenBM
646f10713f Merge branch 'feature-2FA' of github.com:fleetbase/fleetbase into feature-2FA 2024-01-19 20:09:25 +08:00
TemuulenBM
5db723b003 Created two-fa-enforcement component 2024-01-19 20:04:27 +08:00
Ron
74770883fe Merge pull request #185 from fleetbase/dev-v0.3.7
v0.3.7
2024-01-19 19:51:33 +08:00
Ronald A. Richardson
3ebedab90a keep CACHE_DRIVER=null for docker build 2024-01-19 19:48:19 +08:00
Ronald A. Richardson
d0864e822b keep Dockerfile php 7.4 2024-01-19 19:44:11 +08:00
Ronald A. Richardson
f8b564acc9 upgraded dependencies with critical patches 2024-01-19 19:40:46 +08:00
Ronald A. Richardson
fd604c99e0 Critical patches for user verification during onboard and driver creation and loign 2024-01-19 19:27:25 +08:00
Ron
bc0ba3e3fb Merge pull request #184 from fleetbase/dev-v0.3.6
v0.3.6
2024-01-18 19:10:39 +08:00
Ronald A. Richardson
ecbbe002cc updated router.js 2024-01-18 19:10:15 +08:00
Ronald A. Richardson
1f068a890a fix translation file in console 2024-01-18 19:02:16 +08:00
Ronald A. Richardson
3aa365e7ef almost ready for release 2024-01-18 18:59:53 +08:00
Ronald A. Richardson
bb1444dc31 improved and enforced email verification flows 2024-01-18 16:42:42 +08:00
TemuulenBM
09996a4398 Completed validate and change password flow for account
Succesfully implemented Organization Auth settings that is enforces all users to enable or disable 2fa
2024-01-17 19:23:03 +08:00
Bayarbold8
22043a53e3 restore to main storefront 2024-01-17 15:53:00 +08:00
Bayarbold8
acde54c6ae merged with latest 2024-01-17 15:52:23 +08:00
Bayarbold8
e6961089ca iam-engine storefront finished 2024-01-17 14:54:11 +08:00
Ronald A. Richardson
880facdaef merged with latest 2024-01-17 13:55:15 +08:00
Ronald A. Richardson
87c1a56fd1 fix enforce toggle action 2024-01-17 12:14:19 +08:00
Bayarbold8
dba816463f storefront and iam-engine completed 2024-01-17 11:48:56 +08:00
Ronald A. Richardson
270371ce9c refactor for two auth flow and settings 2024-01-16 18:52:52 +08:00
Ronald A. Richardson
9f37b15d8a few patches for 2fa implementation 2024-01-15 18:36:31 +08:00
TemuulenBM
3ac3ad97f3 updated router.map and added tests 2024-01-15 17:12:44 +08:00
Ronald A. Richardson
ad067bd32c merged and updated to latest v0.3.5 2024-01-15 16:52:54 +08:00
TemuulenBM
9bd0cc17d9 Latest push 2024-01-15 16:13:58 +08:00
Ron
fc947d833a Merge pull request #181 from fleetbase/dev-v0.3.5
v0.3.6
2024-01-12 18:39:09 +08:00
Ronald A. Richardson
ff159a04ee upgraded dependencies to latest versions which contain critical patches for API 2024-01-12 18:31:29 +08:00
TemuulenBM
c5ac611000 Tried to verify after verification code is sent 2024-01-12 18:14:24 +08:00
Bayarbold8
863e27bd7b storefront translation finished 2024-01-11 15:32:49 +08:00
TemuulenBM
63c9bc0880 verified resend code 2024-01-10 18:06:44 +08:00
Bayarbold8
6183378717 fixed some mistakes 2024-01-10 13:47:23 +08:00
TemuulenBM
ea35a6f81f Succesfully implemented verify-code process 2024-01-09 18:44:13 +08:00
TemuulenBM
5fb4851544 implemented countdown component in two-fa controller template and succesfully sent sms to user 2024-01-08 18:04:43 +08:00
TemuulenBM
e348b02611 Created 2fa validate session 2024-01-05 18:01:52 +08:00
mm3ddd
3f779969c3 created more translation keys 2024-01-04 18:12:14 +08:00
TemuulenBM
9a23c64dde updated login function within 2fa 2024-01-04 18:06:40 +08:00
mm3ddd
b2098d0cb8 added translation keys for install and invite for user 2024-01-04 13:27:28 +08:00
TemuulenBM
850cc1e20d Fixed 2FA Config component 2024-01-03 17:58:17 +08:00
Ronald A. Richardson
6dd7ca401c complete i18n for auth routes 2024-01-03 17:56:59 +08:00
Ronald A. Richardson
fa51d03cfd (fix) use correct translation key in forgot password route 2024-01-03 17:35:15 +08:00
Ronald A. Richardson
092ebf0589 updated translation file, added translation key for forgot password route 2024-01-03 17:29:18 +08:00
Ron
4f4fdeaafc Merge pull request #176 from fleetbase/dev-v0.3.4
v0.3.4
2023-12-27 11:36:11 +08:00
Ronald A. Richardson
39747601d0 merged with main 2023-12-27 11:29:43 +08:00
Ronald A. Richardson
321ef64229 added truncate to open issues on github card component 2023-12-27 11:27:35 +08:00
Ronald A. Richardson
1b8f7a663d upgraded to core-api v1.3.5 2023-12-27 11:26:43 +08:00
Ronald A. Richardson
2a5a68f620 (fix) urgently replace deprecated route transitionTo with router service method 2023-12-27 11:05:15 +08:00
TemuulenBM
d314964776 updated 2fa page 2023-12-26 17:52:25 +08:00
Ron
3033f6f4cf Merge pull request #175 from fleetbase/dev-v0.3.3
v0.3.3
2023-12-26 12:46:40 +08:00
Ronald A. Richardson
9fbc3252c6 upgraded core-api and fleetops which includes critical patches and fixes 2023-12-26 12:42:04 +08:00
TemuulenBM
9c1655167d created frontend for saving 2023-12-25 18:05:22 +08:00
Ron
25ce216e3b Merge pull request #165 from fleetbase/dev-v0.3.2
v0.3.2
2023-12-25 15:20:12 +08:00
Ronald A. Richardson
6bf3dbad2d move gcp required app.yaml to console dir 2023-12-25 15:07:18 +08:00
Ron
9a8c67a1b7 Merge pull request #168 from ekini/feature/gcp
Add Helm chart and GCP CI/CD pipeline
2023-12-25 15:03:25 +08:00
Ronald A. Richardson
eabd83c8ad upgraded all dependencies to latest releases 2023-12-25 15:03:04 +08:00
TemuulenBM
16e59f3465 Created 2FA page 2023-12-21 18:02:09 +08:00
TemuulenBM
f9d362b1be Created Authentication component for 2FA config 2023-12-20 18:00:35 +08:00
Ronald A. Richardson
e4f64021d7 Added gcs storage as a filesystem driver, updated cors to automatically handle www. in domains 2023-12-12 18:19:28 +08:00
Eugene Dementyev
941c6d03d9 Add socketcluster env vars 2023-12-12 18:41:42 +13:00
Eugene Dementyev
2331c6902e Fix helm port 2023-12-11 21:08:43 +13:00
Eugene Dementyev
be9faea3bc Fix repository 2023-12-11 21:05:30 +13:00
Eugene Dementyev
e8d7c021f5 Fix socketcluster 2023-12-11 21:05:03 +13:00
Eugene Dementyev
8c86eed5fc Update socketcluster 2023-12-11 20:59:11 +13:00
Eugene Dementyev
a2ce7b5a65 Fix repository for helm 2023-12-11 20:26:20 +13:00
Ronald A. Richardson
48069177df Upgrade to 5.4 semi successful, working through deprecations 2023-12-08 13:52:38 +08:00
Ronald A. Richardson
c5574a25ae upgraded console to ember 5.4.1 2023-12-07 11:48:55 +08:00
Ronald A. Richardson
0a3ad2f0a6 added scripts 2023-12-07 11:17:52 +08:00
Ron
d60760104d Merge pull request #163 from fleetbase/feature/update-ignore-file
updated lockfile to ignore private packages
2023-12-07 11:02:50 +08:00
Ronald A. Richardson
88b4acff67 working on upgrade to ember 5.4.1 2023-12-07 10:56:09 +08:00
Eugene Dementyev
4d2179129d Add GCP ci/cd
Add helm chart
2023-12-06 08:01:34 +13:00
Ronald A. Richardson
65a6894d4b updated lockfile to ignore private packages 2023-11-28 11:46:22 +08:00
160 changed files with 13254 additions and 4253 deletions

185
.github/workflows/gcp-cd.yml vendored Normal file
View File

@@ -0,0 +1,185 @@
name: Fleetbase CI/CD
on:
push:
branches: [ "gcpdeploy/*" ]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
env:
PROJECT: ${{ vars.PROJECT }}
REGISTRY: ${{ vars.REGISTRY }}
SOCKETCLUSTER_HOST: ${{ vars.SOCKETCLUSTER_HOST }}
API_HOST: ${{ vars.API_HOST }}
K8S_CLUSTER_NAME: ${{ vars.K8S_CLUSTER_NAME }}
K8S_CLUSTER_LOCATION: ${{ vars.K8S_CLUSTER_LOCATION }}
GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
GCP_SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }}
GCP: "True" # switches docker builds to GCP-style registry
jobs:
build_service:
name: Build and Deploy the Service
runs-on: ubuntu-latest
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set Dynamic ENV Vars
run: |
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c -8)
echo "VERSION=${SHORT_COMMIT}" >> $GITHUB_ENV
echo "STACK=$(basename $GITHUB_REF)" >> $GITHUB_ENV
echo "REGISTRY_HOST=$(dirname $(dirname $REGISTRY))" >> $GITHUB_ENV
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: "access_token"
create_credentials_file: true
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GCP_SERVICE_ACCOUNT }}
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
- id: 'get-credentials'
uses: 'google-github-actions/get-gke-credentials@v1'
with:
cluster_name: ${{ env.K8S_CLUSTER_NAME }}
location: ${{ env.K8S_CLUSTER_LOCATION }}
- uses: 'docker/login-action@v3'
with:
registry: ${{ env.REGISTRY_HOST }}
username: 'oauth2accesstoken'
password: '${{ steps.auth.outputs.access_token }}'
- name: Prepare Composer Auth Secret
run: |
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
echo '{"github-oauth": {"github.com": "'${{ secrets._GITHUB_AUTH_TOKEN }}'"}}' > composer-auth.json
else
echo '{}' > composer-auth.json
fi
- name: nullify ssm-parent config
run: |
# this is needed to disable ssm-parent, which is used on AWS
echo > api/.ssm-parent.yaml
- name: Build and Release
uses: docker/bake-action@v2
env:
REGISTRY: ${{ env.REGISTRY }}
VERSION: ${{ env.VERSION }}
CACHE: type=gha
with:
push: true
files: |
./docker-bake.hcl
- name: deploy with helm
run: |
helm upgrade -i fleetbase infra/helm -n ${{ env.PROJECT }}-${{ env.STACK }} --set image.repository=${{ env.REGISTRY }} --set image.tag=${{ env.VERSION }} --set 'api_host=${{ env.API_HOST }}' --set 'socketcluster_host=${{ env.SOCKETCLUSTER_HOST }}' --set 'ingress.annotations.kubernetes\.io/ingress\.global-static-ip-name=${{ env.PROJECT }}-${{ env.STACK }}'
build_frontend:
name: Build and Deploy the Console
needs: [build_service]
runs-on: ubuntu-latest
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: true
- name: Set Dynamic ENV Vars
run: |
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c -8)
echo "VERSION=${SHORT_COMMIT}" >> $GITHUB_ENV
echo "STACK=$(basename $GITHUB_REF)" >> $GITHUB_ENV
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: "access_token"
create_credentials_file: true
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GCP_SERVICE_ACCOUNT }}
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm Store Directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm Cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Check for _GITHUB_AUTH_TOKEN and create .npmrc
run: |
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
echo "//npm.pkg.github.com/:_authToken=${{ secrets._GITHUB_AUTH_TOKEN }}" > .npmrc
fi
working-directory: ./console
- name: Install dependencies
run: pnpm install
working-directory: ./console
- name: Build
env:
SOCKETCLUSTER_HOST: ${{ env.SOCKETCLUSTER_HOST }}
SOCKETCLUSTER_SECURE: "true"
SOCKETCLUSTER_PORT: "443"
API_HOST: ${{ env.API_HOST }}
run: |
set -eu
pnpm build
working-directory: ./console
- name: Deploy Console 🚀
run: |
set -eu
gcloud app deploy --appyaml console/app.yaml console/dist
# leave 2 versions
gcloud app versions list --filter="traffic_split=0" --sort-by '~version' --format 'value(version.id)' | sed '1d' | xargs -r gcloud app versions delete

21
.gitignore vendored
View File

@@ -17,10 +17,19 @@ api/composer-install-dev.sh
api/auth.json
act.sh
composer-auth.json
packages/billing-api
packages/billing-engine
packages/flespi-engine
packages/flespi-integration
packages/projectargus-engine
docker/database/*
docker/database/mysql/*
docker/database/mysql/*
.talismanrc
verdaccio/minio
verdaccio/config/htpasswd
verdaccio/storage
# private packages
packages/billing
packages/flespi
packages/loconav
packages/internals
packages/projectargus-engine
# wip
packages/solid
solid
verdaccio

3
.gitmodules vendored
View File

@@ -39,6 +39,3 @@
[submodule "docs/api-reference"]
path = docs/api-reference
url = git@github.com:fleetbase/api-reference.git
[submodule "packages/solid"]
path = packages/solid
url = git@github.com:fleetbase/solid.git

View File

@@ -7,7 +7,11 @@ routes:
headers:
Cache-Control: "max-age=600, no-transform, public"
gzip: false
- route: "^.+\\.(html|xml|json)$"
- route: "^.+\\.(xml|json)$"
headers:
Cache-Control: "max-age=600, no-transform, public"
gzip: true
- route: "^.+\\.(html)$"
headers:
Cache-Control: "public, max-age=0, must-revalidate"
gzip: true

View File

@@ -9,10 +9,9 @@
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"fleetbase/core-api": "^1.3.2",
"fleetbase/fleetops-api": "^0.3.5",
"fleetbase/storefront-api": "^0.2.4",
"fleetbase/solid-api": "^0.0.1",
"fleetbase/core-api": "^1.3.12",
"fleetbase/fleetops-api": "^0.4.3",
"fleetbase/storefront-api": "^0.2.8",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.75",
@@ -33,12 +32,6 @@
"nunomaduro/collision": "^5.10",
"phpunit/phpunit": "^9.5.10"
},
"repositories": [
{
"type": "path",
"url": "../packages/solid"
}
],
"autoload": {
"psr-4": {
"App\\": "app/",

1389
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ return [
|
*/
'name' => env('APP_NAME', 'Laravel'),
'name' => env('APP_NAME', 'Fleetbase'),
/*
|--------------------------------------------------------------------------

View File

@@ -1,5 +1,7 @@
<?php
use Fleetbase\Support\Utils;
return [
/*
@@ -19,7 +21,7 @@ return [
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:4200', env('CONSOLE_HOST')],
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_URL'))]),
'allowed_origins_patterns' => [],

View File

@@ -32,13 +32,13 @@ return [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'root' => storage_path('app')
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'url' => env('APP_URL') . '/storage',
],
's3' => [
@@ -51,6 +51,14 @@ return [
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
],
'gcs' => [
'driver' => 'gcs',
'project_id' => env('GOOGLE_CLOUD_PROJECT_ID', 'your-project-id'),
'key_file' => env('GOOGLE_CLOUD_KEY_FILE', null),
'bucket' => env('GOOGLE_CLOUD_STORAGE_BUCKET', env('AWS_BUCKET')),
'path_prefix' => env('GOOGLE_CLOUD_STORAGE_PATH_PREFIX', null),
'storage_api_uri' => env('GOOGLE_CLOUD_STORAGE_API_URI', env('AWS_URL')),
],
],
/*
@@ -65,8 +73,7 @@ return [
*/
'links' => [
public_path('storage') => storage_path('app/public'),
public_path('uploads') => storage_path('app/uploads'),
public_path('storage') => storage_path('app/public')
],
];

View File

@@ -60,6 +60,10 @@ return [
'transport' => 'postmark',
],
'sendgrid' => [
'transport' => 'sendgrid',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),
@@ -96,7 +100,7 @@ return [
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@fleetbase.io'),
'name' => env('MAIL_FROM_NAME', 'Fleetbase'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Fleetbase')),
],
/*

View File

@@ -30,4 +30,8 @@ return [
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'sendgrid' => [
'api_key' => env('SENDGRID_API_KEY'),
],
];

View File

@@ -13,4 +13,7 @@ php artisan migrate --force
php artisan sandbox:migrate --force
# Seed database
php artisan fleetbase:seed
php artisan fleetbase:seed
# Restart queue
php artisan queue:restart

5622
api/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
/var/www/html/api/storage/app/uploads

View File

@@ -1,15 +1,7 @@
{
/**
Ember CLI sends analytics information by default. The data is completely
anonymous, but there are times when you might want to disable this behavior.
Setting `disableAnalytics` to true will prevent any data from being sent.
*/
"disableAnalytics": false,
/**
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
*/
"isTypeScriptProject": false
}

View File

@@ -1,25 +1,13 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
!.*
.*/
.eslintcache
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/npm-shrinkwrap.json.ember-try
/package.json.ember-try
/package-lock.json.ember-try
/yarn.lock.ember-try

View File

@@ -2,12 +2,13 @@
module.exports = {
root: true,
parser: 'babel-eslint',
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2018,
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
legacyDecorators: true,
requireConfigFile: false,
babelOptions: {
plugins: [['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }]],
},
},
plugins: ['ember'],
@@ -29,7 +30,7 @@ module.exports = {
'ember/no-empty-glimmer-component-classes': 'off',
'ember/no-get': 'off',
'ember/classic-decorator-no-classic-methods': 'off',
'node/no-unpublished-require': [
'n/no-unpublished-require': [
'error',
{
allowModules: [
@@ -50,18 +51,18 @@ module.exports = {
'no-prototype-builtins': 'off',
},
overrides: [
// node files
{
files: [
'./.eslintrc.js',
'./.prettierrc.js',
'./.stylelintrc.js',
'./.template-lintrc.js',
'./ember-cli-build.js',
'./index.js',
'./testem.js',
'./blueprints/*/index.js',
'./config/**/*.js',
'./tests/dummy/config/**/*.js',
'./lib/*/index.js',
'./server/**/*.js',
],
parserOptions: {
sourceType: 'script',
@@ -70,13 +71,7 @@ module.exports = {
browser: false,
node: true,
},
plugins: ['node'],
extends: ['plugin:node/recommended'],
},
{
// test files
files: ['tests/**/*-test.{js,ts}'],
extends: ['plugin:qunit/recommended'],
extends: ['plugin:n/recommended'],
},
],
};

View File

@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
node-version: [16.x] # Build on Node.js 16
node-version: [18.x] # Build on Node.js 18
steps:
- uses: actions/checkout@v2
@@ -57,4 +57,4 @@ jobs:
run: pnpm run lint
- name: Build
run: npx ember build --environment production
run: npx ember build --environment production

10
console/.gitignore vendored
View File

@@ -1,22 +1,17 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist/
/tmp/
/declarations/
# dependencies
/bower_components/
/node_modules/
/scripts/node_modules/
# misc
/.env*
/environments/.env*
/.pnp*
/.sass-cache
/.eslintcache
/connect.lock
/coverage/
/libpeerconnection.log
/npm-debug.log*
/testem.log
/yarn-error.log
@@ -24,7 +19,6 @@
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/npm-shrinkwrap.json.ember-try
/package.json.ember-try
/package-lock.json.ember-try

View File

@@ -1,25 +1,13 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
!.*
.eslintcache
.lint-todo/
.*/
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/npm-shrinkwrap.json.ember-try
/package.json.ember-try
/package-lock.json.ember-try
/yarn.lock.ember-try

View File

@@ -8,7 +8,7 @@ module.exports = {
printWidth: 190,
overrides: [
{
files: '*.hbs',
files: '*.{hbs,js,ts}',
options: {
singleQuote: false,
},

8
console/.stylelintignore Normal file
View File

@@ -0,0 +1,8 @@
# unconventional files
/blueprints/*/files/
# compiled output
/dist/
# addons
/.node_modules.ember-try/

8
console/.stylelintrc.js Normal file
View File

@@ -0,0 +1,8 @@
'use strict';
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
rules: {
'import-notation': null,
},
};

View File

@@ -6,7 +6,6 @@ module.exports = {
'no-bare-strings': 'off',
'no-invalid-interactive': 'off',
'no-yield-only': 'off',
'no-down-event-binding': 'off',
'table-groups': 'off',
'link-href-attributes': 'off',
'require-input-label': 'off',

View File

@@ -1,3 +1,3 @@
{
"ignore_dirs": ["tmp", "dist"]
"ignore_dirs": ["dist"]
}

View File

@@ -1,5 +1,5 @@
# ---- Build Stage ----
FROM node:16.20-alpine AS builder
FROM node:18.15.0-alpine AS builder
# Set the working directory in the container to /app
WORKDIR /app

12
console/app.yaml Normal file
View File

@@ -0,0 +1,12 @@
runtime: python312
handlers:
- url: /(.*\..+)$
static_files: \1
upload: (.+)
secure: always
expiration: 1h
- url: /.*
static_files: index.html
upload: index.html
secure: always

View File

@@ -0,0 +1 @@
{{yield}}

View File

@@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class Configure2faComponent extends Component {}

View File

@@ -5,14 +5,14 @@
<Spinner />
</div>
{{else}}
<div class="flex flex-row p-4 border-b dark:border-gray-700 border-gray-200">
<div class="flex flex-row p-3 border-b dark:border-gray-700 border-gray-200">
<div class="w-12 flex-shrink-0"><img src={{this.data.owner.avatar_url}} alt="fleetbase/fleetbase" class="rounded-full w-8 h-8" width="32" height="32" /></div>
<div class="flex-1 -mt-2">
<div class="flex flex-1 flex-row items-center justify-between mb-2">
<a href={{this.data.html_url}} target="_github" class="dark:text-gray-100 text-black text-lg font-semibold">{{this.data.full_name}}</a>
<a href={{this.data.html_url}} target="_github" class="dark:text-gray-100 text-black text-base font-semibold">{{this.data.full_name}}</a>
<a href={{this.data.html_url}} target="_github" class="btn btn-xs btn-default">
<FaIcon @icon="star" class="text-yellow-400" />
<span class="hidden lg:flex ml-2.5">Star on Github</span>
<span class="hidden truncate lg:flex ml-2.5">Star on Github</span>
</a>
</div>
<p class="dark:text-gray-100 text-black text-sm">{{this.data.description}}</p>
@@ -40,7 +40,7 @@
</div>
<div class="col-span-3 dark:text-gray-100 text-black text-xs flex flex-row">
<span class="font-bold mr-1">{{this.data.open_issues_count}}</span>
<span><span class="hidden lg:inline-flex">Open</span> Issues</span>
<span class="truncate"><span class="hidden lg:inline-flex">Open</span> Issues</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
<div class="next-user-button" ...attributes>
<BasicDropdown @defaultClass={{@wrapperClass}} @onOpen={{@onOpen}} @onClose={{@onClose}} @verticalPosition={{@verticalPosition}} @horizontalPosition={{@horizontalPosition}} @renderInPlace={{or @renderInPlace true}} @initiallyOpened={{@initiallyOpened}} as |dd|>
<dd.Trigger class={{@triggerClass}}>
<div class="next-org-button-trigger flex-shrink-0 {{if dd.isOpen 'is-open'}}">
<FaIcon @icon="globe" @size="sm" />
</div>
</dd.Trigger>
<dd.Content class={{@contentClass}}>
<div class="next-dd-menu {{@dropdownMenuClass}} {{if dd.isOpen 'is-open'}}">
{{#each-in this.availableLocales as |key country|}}
<div class="px-1">
<a href="javascript:;" class="next-dd-item" {{on "click" (fn this.changeLocale key)}}>
<div class="flex flex-row items-center justify-between w-full">
<div class="flex-1">
<span class="mr-1">{{country.emoji}}</span>
<span>{{country.language}}</span>
</div>
{{#if (eq this.currentLocale key)}}
<div>
<FaIcon @icon="check" class="text-green-400" />
</div>
{{/if}}
</div>
</a>
</div>
{{/each-in}}
</div>
</dd.Content>
</BasicDropdown>
</div>

View File

@@ -0,0 +1,144 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency-decorators';
export default class LocaleSelectorComponent extends Component {
/**
* Inject the intl service.
*
* @memberof LocaleSelectorComponent
*/
@service intl;
/**
* Inject the intl service.
*
* @memberof LocaleSelectorComponent
*/
@service fetch;
/**
* Tracks all the available locales.
*
* @memberof LocaleSelectorComponent
*/
@tracked locales = [];
/**
* All available countries data.
*
* @memberof LocaleSelectorComponent
*/
@tracked countries = [];
/**
* The current locale in use.
*
* @memberof LocaleSelectorComponent
*/
@tracked currentLocale;
/**
* Creates an instance of LocaleSelectorComponent.
* @memberof LocaleSelectorComponent
*/
constructor() {
super(...arguments);
this.locales = this.intl.locales;
this.currentLocale = this.intl.primaryLocale;
this.loadAvailableCountries.perform();
// Check for locale change
this.intl.onLocaleChanged(() => {
this.currentLocale = this.intl.primaryLocale;
});
}
/**
* Handles the change of locale.
* @param {string} selectedLocale - The selected locale.
* @returns {void}
* @memberof LocaleSelectorComponent
* @method changeLocale
* @instance
* @action
*/
@action changeLocale(selectedLocale) {
this.currentLocale = selectedLocale;
this.intl.setLocale(selectedLocale);
// Persist to server
this.saveUserLocale.perform(selectedLocale);
}
/**
* Loads available countries asynchronously.
* @returns {void}
* @memberof LocaleSelectorComponent
* @method loadAvailableCountries
* @instance
* @task
* @generator
*/
@task *loadAvailableCountries() {
this.countries = yield this.fetch.get('lookup/countries', { columns: ['name', 'cca2', 'flag', 'emoji', 'languages'] });
this.availableLocales = this._createAvailableLocaleMap();
}
/**
* Saves the user's selected locale to the server.
* @param {string} locale - The user's selected locale.
* @returns {void}
* @memberof LocaleSelectorComponent
* @method saveUserLocale
* @instance
* @task
* @generator
*/
@task *saveUserLocale(locale) {
yield this.fetch.post('users/locale', { locale });
}
/**
* Creates a map of available locales.
* @private
* @returns {Object} - The map of available locales.
* @memberof LocaleSelectorComponent
* @method _createAvailableLocaleMap
* @instance
*/
_createAvailableLocaleMap() {
const localeMap = {};
for (let i = 0; i < this.locales.length; i++) {
const locale = this.locales.objectAt(i);
localeMap[locale] = this._findCountryDataForLocale(locale);
}
return localeMap;
}
/**
* Finds country data for a given locale.
* @private
* @param {string} locale - The locale to find country data for.
* @returns {Object|null} - The country data or null if not found.
* @memberof LocaleSelectorComponent
* @method _findCountryDataForLocale
* @instance
*/
_findCountryDataForLocale(locale) {
const localeCountry = locale.split('-')[1];
const country = this.countries.find((country) => country.cca2.toLowerCase() === localeCountry);
if (country) {
// get the language
country.language = Object.values(country.languages)[0];
}
return country;
}
}

View File

@@ -0,0 +1,14 @@
{{#if this.shouldRender}}
<InfoBlock @icon="triangle-exclamation" class="two-fa-enforcement-alert bg-yellow-100 border-2 border-yellow-600 dark:border-yellow-500 rounded-lg py-3.5 px-5">
<div class="flex flex-row justify-between">
<div class="flex-1 pr-2">
<p class="text-sm dark:text-yellow-900 mb-2">
{{t "component.two-fa-enforcement-alert.message"}}
</p>
</div>
<div class="flex-shrink-0">
<Button id="two-fa-setup-button" @text={{t "component.two-fa-enforcement-alert.button-text"}} @icon="shield-halved" @type="warning" @buttonType="button" @onClick={{this.transitionToTwoFactorSettings}} />
</div>
</div>
</InfoBlock>
{{/if}}

View File

@@ -0,0 +1,74 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { task } from 'ember-concurrency-decorators';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
/**
* Glimmer component for handling notification enforcement.
*
* @class EnforceNotificationComponent
* @extends Component
*/
export default class TwoFaEnforcementAlertComponent extends Component {
/**
* Flag to determine whether the component should render or not.
*
* @property {boolean} shouldRender
* @default false
* @tracked
*/
@tracked shouldRender = false;
/**
* Ember Router service for transitioning between routes.
*
* @type {RouterService}
*/
@service router;
/**
* Fetch service for making HTTP requests.
*
* @property {FetchService} fetch
* @inject
*/
@service fetch;
/**
* Constructor method for the ConsoleAccountAuthController.
*
* @constructor
*/
constructor() {
super(...arguments);
this.checkTwoFactorEnforcement.perform();
}
/**
* Transition to the users auth page.
*
* @method transitionToTwoFa
* @memberof ConsoleHomeController
*/
@action transitionToTwoFactorSettings() {
this.router.transitionTo('console.account.auth');
}
@task *checkTwoFactorEnforcement() {
const shouldRender = yield this.fetch.get('two-fa/enforce').catch((error) => {
this.notifications.serverError(error);
});
/**
* Task to check whether two-factor authentication enforcement is required.
*
* @method checkTwoFactorEnforcement
* @memberof TwoFaEnforcementAlertComponent
* @task
*/
if (shouldRender) {
this.shouldRender = shouldRender.shouldEnforce;
}
}
}

View File

@@ -0,0 +1,31 @@
<div class="flex items-center space-x-4">
<label class="text-base font-medium">Enable Two-Factor Authentication</label>
<Toggle @isToggled={{this.isTwoFaEnabled}} @onToggle={{this.onTwoFaToggled}} />
</div>
{{#if this.isTwoFaEnabled}}
<div class="mt-6">
{{#if this.showEnforceOption}}
<div class="flex items-center space-x-4 mb-6">
<label class="text-base font-medium">Require Users to Set-Up 2FA</label>
<Toggle @isToggled={{this.isTwoFaEnforced}} @onToggle={{this.onTwoFaEnforcedToggled}} />
</div>
{{/if}}
{{#if this.showMethodSelection}}
<label class="text-base font-medium">Choose an authentication method</label>
<p class="text-sm text-gray-600 dark:text-gray-300 mt-1">In addition to your username and password, you'll have to enter a code (delivered via app or SMS) to sign in to your account</p>
{{#each @twoFaMethods as |method|}}
<div class="border rounded-lg px-4 py-3 mt-2 transition duration-300 {{if (eq this.selectedTwoFaMethod method.key) 'border-blue-500' 'border-gray-200 dark:border-gray-700'}}">
<input type="radio" name="2fa-method" id="{{method.name}}" checked={{eq this.selectedTwoFaMethod method.key}} {{on "change" (fn this.onTwoFaSelected method.key)}} />
<label for="{{method.name}}">
{{method.name}}
{{#if method.recommended}}
<span class="bg-blue-500 rounded-xl text-white px-2 py-1 ml-2 text-xs font-semibold">Recommended</span>
{{/if}}
<p class="text-sm text-gray-600 dark:text-gray-300 mt-1">{{method.description}}</p>
</label>
</div>
{{/each}}
{{/if}}
</div>
{{/if}}

View File

@@ -0,0 +1,169 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { inject as service } from '@ember/service';
/**
* Default Two-Factor Authentication method when not explicitly selected.
*
* @property {string} DEFAULT_2FA_METHOD
* @private
*/
const DEFAULT_2FA_METHOD = 'email';
/**
* Glimmer component for managing Two-Factor Authentication settings.
*
* @class TwoFaSettingsComponent
* @extends Component
*/
export default class TwoFaSettingsComponent extends Component {
/**
* The fetch service for making HTTP requests.
*
* @property {Service} fetch
* @public
*/
@service fetch;
/**
* The notifications service for displaying user notifications.
*
* @property {Service} notifications
* @public
*/
@service notifications;
/**
* The currently selected Two-Factor Authentication method.
*
* @property {string} selectedTwoFaMethod
* @public
*/
@tracked selectedTwoFaMethod;
/**
* Indicates whether Two-Factor Authentication is currently enabled.
*
* @property {boolean} isTwoFaEnabled
* @public
*/
@tracked isTwoFaEnabled;
/**
* Indicates whether Two-Factor Authentication is required for all users.
*
* @property {boolean} isTwoFaEnforced
* @public
*/
@tracked isTwoFaEnforced;
/**
* Indicates whether the settings should render an option to `enforce`
* Enforce is a flag that indicates that users either under a company or system must setup 2FA.
*
* @property {boolean} showEnforceOption
* @public
*/
@tracked showEnforceOption;
/**
* Indicates whether the settings should render an option to select 2fa `mn=ethod`
* Method is a flag that indicates which method users can receive a 2FA code from.
*
* @property {boolean} showEnforceOption
* @public
*/
@tracked showMethodSelection;
/**
* Class constructor to initialize the component.
*
* @constructor
* @param {Object} owner - The owner of the component.
* @param {Object} options - Options passed during component instantiation.
* @param {Object} options.twoFaSettings - The current Two-Factor Authentication settings.
* @param {Array} options.twoFaMethods - Available Two-Factor Authentication methods.
*/
constructor(owner, { twoFaSettings, twoFaMethods, showEnforceOption, showMethodSelection = true }) {
super(...arguments);
const userSelectedMethod = isArray(twoFaMethods) ? twoFaMethods.find(({ key }) => key === twoFaSettings.method) : null;
this.showMethodSelection = showMethodSelection === true;
this.showEnforceOption = showEnforceOption === true;
this.isTwoFaEnabled = twoFaSettings.enabled === true;
this.isTwoFaEnforced = twoFaSettings.enforced === true;
this.selectedTwoFaMethod = userSelectedMethod ? userSelectedMethod.key : DEFAULT_2FA_METHOD;
}
/**
* Action handler for toggling Two-Factor Authentication.
*
* @method onTwoFaToggled
* @param {boolean} isTwoFaEnabled - Indicates whether Two-Factor Authentication is enabled.
* @return {void}
* @public
*/
@action onTwoFaToggled(isTwoFaEnabled) {
this.isTwoFaEnabled = isTwoFaEnabled;
if (isTwoFaEnabled) {
const recommendedMethod = isArray(this.args.twoFaMethods) ? this.args.twoFaMethods.find((method) => method.recommended) : null;
if (recommendedMethod) {
this.selectedTwoFaMethod = recommendedMethod.key;
}
} else {
this.selectedTwoFaMethod = null;
}
if (typeof this.args.onTwoFaToggled === 'function') {
this.args.onTwoFaToggled(...arguments);
}
if (typeof this.args.onTwoFaMethodSelected === 'function') {
this.args.onTwoFaMethodSelected(this.selectedTwoFaMethod);
}
}
/**
* Action handler for toggling Two-Factor Authentication.
*
* @method onTwoFaEnforcedToggled
* @param {boolean} isTwoFaEnforced - Indicates whether Two-Factor Authentication is enabled.
* @return {void}
* @public
*/
@action onTwoFaEnforcedToggled(isTwoFaEnforced) {
this.isTwoFaEnforced = isTwoFaEnforced;
if (typeof this.args.onTwoFaEnforcedToggled === 'function') {
this.args.onTwoFaEnforcedToggled(...arguments);
}
}
/**
* Action handler for selecting a Two-Factor Authentication method.
*
* @method onTwoFaSelected
* @param {string} method - The selected Two-Factor Authentication method.
* @return {void}
* @public
*/
@action onTwoFaSelected(method) {
this.selectedTwoFaMethod = method;
if (typeof this.args.onTwoFaMethodSelected === 'function') {
this.args.onTwoFaMethodSelected(...arguments);
}
}
@action onRequireUsersToSetUpToggled(isTwoFaEnforced) {
this.isTwoFaEnforced = isTwoFaEnforced;
if (typeof this.args.onTwoFaEnforcedToggled === 'function') {
this.args.onTwoFaEnforcedToggled(isTwoFaEnforced);
}
}
}

View File

@@ -18,6 +18,13 @@ export default class AuthForgotPasswordController extends Controller {
*/
@service notifications;
/**
* Inject the `intl` service
*
* @memberof AuthForgotPasswordController
*/
@service intl;
/**
* The email variable
*
@@ -55,7 +62,7 @@ export default class AuthForgotPasswordController extends Controller {
this.fetch
.post('auth/get-magic-reset-link', { email })
.then(() => {
this.notifications.success('Check your email to continue!');
this.notifications.success(this.intl.t('auth.forgot-password.success-message'));
this.isSent = true;
})
.catch((error) => {

View File

@@ -33,6 +33,27 @@ export default class AuthLoginController extends Controller {
*/
@service session;
/**
* Inject the `router` service
*
* @var {Service}
*/
@service router;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `fetch` service
*
* @var {Service}
*/
@service fetch;
/**
* Whether or not to remember the users session
*
@@ -89,29 +110,62 @@ export default class AuthLoginController extends Controller {
*/
@tracked failedAttempts = 0;
/**
* Authenticate the user
*
* @void
*/
@tracked token;
@action async login(event) {
// firefox patch
event.preventDefault();
// get user credentials
const { email, password, rememberMe } = this;
const { identity, password, rememberMe } = this;
// If no password error
if (!identity) {
return this.notifications.warning(this.intl.t('auth.login.no-identity-notification'));
}
// If no password error
if (!password) {
return this.notifications.warning(this.intl.t('auth.login.no-identity-notification'));
}
// start loader
this.set('isLoading', true);
// set where to redirect on login
this.setRedirect();
// send request to check for 2fa
try {
await this.session.authenticate('authenticator:fleetbase', { email, password }, rememberMe);
let { twoFaSession, isTwoFaEnabled } = await this.session.checkForTwoFactor(identity);
if (isTwoFaEnabled) {
return this.session.store
.persist({ identity })
.then(() => {
return this.router.transitionTo('auth.two-fa', { queryParams: { token: twoFaSession } }).then(() => {
this.reset('success');
});
})
.catch((error) => {
this.notifications.serverError(error);
this.reset('error');
throw error;
});
}
} catch (error) {
return this.notifications.serverError(error);
}
try {
await this.session.authenticate('authenticator:fleetbase', { identity, password }, rememberMe);
} catch (error) {
this.failedAttempts++;
// Handle unverified user
if (error.toString().includes('not verified')) {
return this.sendUserForEmailVerification(identity);
}
return this.failure(error);
}
@@ -124,20 +178,44 @@ export default class AuthLoginController extends Controller {
* Transition user to onboarding screen
*/
@action transitionToOnboard() {
return this.transitionToRoute('onboard');
return this.router.transitionTo('onboard');
}
/**
* Transition to forgot password screen, if email is set - set it.
*/
@action forgotPassword() {
return this.transitionToRoute('auth.forgot-password').then(() => {
return this.router.transitionTo('auth.forgot-password').then(() => {
if (this.email) {
this.forgotPasswordController.email = this.email;
}
});
}
/**
* Creates an email verification session and transitions user to verification route.
*
* @param {String} email
* @return {Promise<Transition>}
* @memberof AuthLoginController
*/
sendUserForEmailVerification(email) {
return this.fetch.post('auth/create-verification-session', { email, send: true }).then(({ token }) => {
return this.session.store.persist({ email }).then(() => {
this.notifications.warning(this.intl.t('auth.login.unverified-notification'));
return this.router
.transitionTo('auth.verification', {
queryParams: {
token,
},
})
.then(() => {
this.reset('error');
});
});
});
}
/**
* Sets correct route to send user to after login.
*
@@ -177,7 +255,7 @@ export default class AuthLoginController extends Controller {
* @void
*/
slowConnection() {
this.notifications.error('Experiencing connectivity issues.');
this.notifications.error(this.intl.t('auth.login.slow-connection-message'));
}
/**

View File

@@ -18,6 +18,20 @@ export default class AuthResetPasswordController extends Controller {
*/
@service notifications;
/**
* Inject the `router` service
*
* @memberof AuthResetPasswordController
*/
@service router;
/**
* Inject the `intl` service
*
* @memberof AuthResetPasswordController
*/
@service intl;
/**
* The code param.
*
@@ -63,9 +77,9 @@ export default class AuthResetPasswordController extends Controller {
this.fetch
.post('auth/reset-password', { link: id, code, password, password_confirmation })
.then(() => {
this.notifications.success('Your password has been reset! Login to continue.');
this.notifications.success(this.intl.t('auth.reset-password.success-message'));
return this.transitionToRoute('auth.login');
return this.router.transitionTo('auth.login');
})
.catch((error) => {
this.notifications.serverError(error);

View File

@@ -0,0 +1,271 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
/**
* Controller responsible for handling two-factor authentication.
* @class AuthTwoFaController
* @extends Controller
*/
export default class AuthTwoFaController extends Controller {
/**
* Router service.
*
* @var {Service}
*/
@service router;
/**
* Fetch service for making HTTP requests.
*
* @var {Service}
*/
@service fetch;
/**
* Notifications service for handling notifications.
*
* @var {Service}
*/
@service notifications;
/**
* Session service for managing user sessions.
*
* @var {Service}
*/
@service session;
/**
* Internationalization service.
*
* @var {Service}
*/
@service intl;
/**
* Tracked property for storing the verification token.
*
* @property {string} token
* @tracked
*/
@tracked token;
/**
* The current 2FA identity in memory
*/
@tracked identity;
/**
* Tracked property representing the client token from the validated 2fa session.
*
* @property {number} clientToken
* @tracked
* @default null
*/
@tracked clientToken;
/**
* Tracked property for storing the verification code.
*
* @property {string} verificationCode
* @tracked
*/
@tracked verificationCode = '';
/**
* Tracked property for storing the verification code.
*
* @property {string} verificationCode
* @tracked
*/
@tracked otpValue = '';
/**
* Tracked property representing the date the 2fa session will expire
* @property {Date|null} twoFactorSessionExpiresAfter
* @tracked
* @default null
*/
@tracked twoFactorSessionExpiresAfter;
/**
* Tracked property representing when the countdown is ready to start.
*
* @property {Boolean} countdownReady
* @tracked
* @default false
*/
@tracked countdownReady = false;
/**
* Tracked property representing when verification code has expired.
*
* @property {Boolean} isCodeExpired
* @tracked
* @default false
*/
@tracked isCodeExpired = false;
/**
* Query parameters for the controller.
*
* @property {Array} queryParams
*/
queryParams = ['token', 'clientToken'];
/**
* Action method for verifying the entered verification code.
*
* @method verifyCode
* @action
*/
@action async verifyCode(event) {
// prevent form default behaviour
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}
try {
const { token, verificationCode, clientToken, identity } = this;
if (!clientToken) {
this.notifications.error(this.intl.t('auth.two-fa.verify-code.invalid-session-error-notification'));
return;
}
// Call the backend API to verify the entered verification code
const { authToken } = await this.fetch.post('two-fa/verify', {
token,
code: verificationCode,
clientToken,
identity,
});
// If verification is successful, transition to the desired route
this.notifications.success(this.intl.t('auth.two-fa.verify-code.verification-successful-notification'));
// authenticate user
return this.session.authenticate('authenticator:fleetbase', { authToken }).then(() => {
return this.router.transitionTo('console');
});
} catch (error) {
if (error.message.includes('Verification code has expired')) {
this.notifications.info(this.intl.t('auth.two-fa.verify-code.verification-code-expired-notification'));
} else {
this.notifications.error(this.intl.t('auth.two-fa.verify-code.verification-code-failed-notification'));
}
}
}
/**
* Resends the verification code for Two-Factor Authentication.
* Disables the countdown timer while processing and handles success or error notifications.
*
* @returns {Promise<void>}
* @action
*/
@action async resendCode() {
// disable countdown timer
this.countdownReady = false;
try {
const { identity, token } = this;
const { clientToken } = await this.fetch.post('two-fa/resend', {
identity,
token,
});
if (clientToken) {
this.clientToken = clientToken;
this.twoFactorSessionExpiresAfter = this.getExpirationDateFromClientToken(clientToken);
this.countdownReady = true;
this.isCodeExpired = false;
this.notifications.success(this.intl.t('auth.two-fa.resend-code.verification-code-resent-notification'));
} else {
this.notifications.error(this.intl.t('auth.two-fa.resend-code.verification-code-resent-error-notification'));
}
} catch (error) {
// Handle errors, show error notifications, etc.
this.notifications.error(this.intl.t('auth.two-fa.resend-code.verification-code-resent-error-notification'));
}
}
/**
* Cancels the current Two-Fa session and redirects to login screen.
*
* @returns {Promise<Transition>}
* @memberof AuthTwoFaController
*/
@action cancelTwoFactor() {
return this.fetch
.post('two-fa/invalidate', {
identity: this.identity,
token: this.token,
})
.then(() => {
return this.router.transitionTo('auth.login');
});
}
/**
* Set that the verification code has expired and allow user to resend.
*
* @memberof AuthTwoFaController
*/
@action handleCodeExpired() {
this.isCodeExpired = true;
this.countdownReady = false;
}
/**
* Handles the input of the OTP (One-Time Password) and triggers the verification process.
*
* @param {string} otpValue - The OTP value entered by the user.
* @returns {void}
* @action
*/
@action handleOtpInput(otpValue) {
this.verificationCode = otpValue;
this.verifyCode();
}
/**
* Converts a base64 encoded client token to a Date representing the expiration date.
*
* @method getExpirationDateFromClientToken
* @param {string} clientToken - Base64 encoded client token.
* @returns {Date|null} - Date representing the expiration date, or null if invalid.
*/
getExpirationDateFromClientToken(clientToken) {
const decoder = new TextDecoder();
const binString = atob(clientToken);
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0));
const decodedString = decoder.decode(bytes);
if (typeof decodedString === 'string' && decodedString.includes('|')) {
const parts = decodedString.split('|');
const expiresAt = this.convertUtcToClientTime(parts[0]);
if (expiresAt instanceof Date) {
return expiresAt;
}
}
return null;
}
/**
* Converts a UTC date-time string to client time zone.
*
* @method convertUtcToClientTime
* @param {string} utcDateTimeString - UTC date-time string.
* @returns {Date} - Date in client time zone.
*/
convertUtcToClientTime(utcDateTimeString) {
const utcDate = new Date(utcDateTimeString);
const clientTimezoneOffset = new Date().getTimezoneOffset();
const clientDate = new Date(utcDate.getTime() - clientTimezoneOffset * 60 * 1000);
return clientDate;
}
}

View File

@@ -0,0 +1,230 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { not } from '@ember/object/computed';
export default class AuthVerificationController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof OnboardIndexController
*/
@service fetch;
/**
* Inject the `notifications` service
*
* @memberof OnboardIndexController
*/
@service notifications;
/**
* Inject the `modalsManager` service
*
* @memberof OnboardIndexController
*/
@service modalsManager;
/**
* Inject the `currentUser` service
*
* @memberof OnboardIndexController
*/
@service currentUser;
/**
* Inject the `router` service
*
* @memberof OnboardIndexController
*/
@service router;
/**
* Inject the `session` service
*
* @memberof OnboardIndexController
*/
@service session;
/**
* The session paramerer.
*
* @memberof OnboardVerifyEmailController
*/
@tracked hello;
/**
* The token paramerer.
*
* @memberof OnboardVerifyEmailController
*/
@tracked token;
/**
* The loading state of the verification request.
*
* @memberof OnboardVerifyEmailController
*/
@tracked isLoading = false;
/**
* Validation state tracker.
*
* @memberof OnboardVerifyEmailController
*/
@tracked isReadyToSubmit = false;
/**
* The request timeout to trigger alternative verification options.
*
* @memberof OnboardVerifyEmailController
*/
@tracked waitTimeout = 1000 * 60 * 1.25;
/**
* Determines if Fleetbase is still awaiting verification after a certain time.
*
* @memberof OnboardVerifyEmailController
*/
@tracked stillWaiting = false;
/**
* the input code.
*
* @memberof OnboardVerifyEmailController
*/
@tracked code;
/**
* The query param for the session token.
*
* @memberof OnboardVerifyEmailController
*/
@tracked queryParams = ['hello', 'token'];
/**
* The boolean opposite of `isReadyToSubmit`
*
* @memberof OnboardVerifyEmailController
*/
@not('isReadyToSubmit') isNotReadyToSubmit;
/**
* Creates an instance of OnboardVerifyEmailController.
* @memberof OnboardVerifyEmailController
*/
constructor() {
super(...arguments);
later(
this,
() => {
this.stillWaiting = true;
},
this.waitTimeout
);
}
/**
* Validates the input
*
* @param {InputEvent} { target: { value } }
* @memberof OnboardVerifyEmailController
*/
@action validateInput({ target: { value } }) {
if (value.length > 5) {
this.isReadyToSubmit = true;
} else {
this.isReadyToSubmit = false;
}
}
/**
* Submits to verify code.
*
* @return {Promise}
* @memberof OnboardVerifyEmailController
*/
@action verifyCode() {
const { token, code, email } = this;
this.isLoading = true;
return this.fetch
.post('auth/verify-email', { token, code, email, authenticate: true })
.then(({ status, token }) => {
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.session.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
}
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
/**
* Action to resend verification code by SMS.
*
* @memberof OnboardVerifyEmailController
*/
@action resendBySms() {
this.modalsManager.show('modals/verify-by-sms', {
title: 'Verify Account by Phone',
acceptButtonText: 'Send',
phone: this.currentUser.phone,
confirm: (modal) => {
modal.startLoading();
const phone = modal.getOption('phone');
return this.fetch
.post('onboard/send-verification-sms', { phone, session: this.hello })
.then(() => {
this.notifications.success('Verification code SMS sent!');
})
.catch((error) => {
this.notifications.serverError(error);
});
},
});
}
/**
* Action to resend verification code by email.
*
* @memberof OnboardVerifyEmailController
*/
@action resendEmail() {
this.modalsManager.show('modals/resend-verification-email', {
title: 'Resend Verification Code',
acceptButtonText: 'Send',
email: this.currentUser.email,
confirm: (modal) => {
modal.startLoading();
const email = modal.getOption('email');
return this.fetch
.post('onboard/send-verification-email', { email, session: this.hello })
.then(() => {
this.notifications.success('Verification code email sent!');
})
.catch((error) => {
this.notifications.serverError(error);
});
},
});
}
}

View File

@@ -72,6 +72,20 @@ export default class ConsoleController extends Controller {
*/
@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.
*
@@ -101,18 +115,49 @@ export default class ConsoleController extends Controller {
*/
constructor() {
super(...arguments);
this.router.on('routeDidChange', (transition) => {
if (this.sidebarContext) {
if (this.hiddenSidebarRoutes.includes(transition.to.name)) {
// Determine if the new route should hide the sidebar
const shouldHideSidebar = this.hiddenSidebarRoutes.includes(transition.to.name);
// Check if the sidebar was manually toggled and is currently closed
const isSidebarManuallyClosed = this.sidebarToggleState.clicked && !this.sidebarToggleState.isOpen;
// Hide the sidebar if the current route is in hiddenSidebarRoutes
if (shouldHideSidebar) {
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.sidebarContext.hideNow();
} else {
// Otherwise, show the sidebar
this.sidebarContext.show();
}
// Ensure toggle is enabled unless on a hidden route
this.sidebarToggleEnabled = !shouldHideSidebar;
}
});
}
/**
* When sidebar is manually toggled
*
* @param {SidebarContext} sidebar
* @param {boolean} isOpen
* @memberof ConsoleController
*/
@action onSidebarToggle(sidebar, isOpen) {
this.sidebarToggleState = {
clicked: true,
isOpen,
};
}
/**
* Sets the sidebar context
*
@@ -125,6 +170,7 @@ export default class ConsoleController extends Controller {
if (this.hiddenSidebarRoutes.includes(this.router.currentRouteName)) {
this.sidebarContext.hideNow();
this.sidebarToggleEnabled = false;
}
}

View File

@@ -0,0 +1,248 @@
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-decorators';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
/**
* Controller for managing user authentication and password-related actions in the console.
*
* @class ConsoleAccountAuthController
* @extends Controller
*/
export default class ConsoleAccountAuthController extends Controller {
/**
* Service for handling data fetching.
*
* @type {fetch}
*/
@service fetch;
/**
* Service for displaying notifications.
*
* @type {notifications}
*/
@service notifications;
/**
* Service for managing application routing.
*
* @type {router}
*/
@service router;
/**
* The user's current password.
* @type {string}
*/
@tracked password;
/**
* The user's confirmation of the new password.
*
* @type {string}
*/
@tracked confirmPassword;
/**
* The new password the user intends to set.
*
* @type {string}
*/
@tracked newPassword;
/**
* The user's confirmation of the new password.
*
* @type {string}
*/
@tracked newConfirmPassword;
/**
* Flag indicating whether the current password has been validated.
*
* @type {boolean}
*/
@tracked isPasswordValidated = false;
/**
* System-wide two-factor authentication configuration.
*
* @type {Object}
*/
@tracked twoFaConfig = {};
/**
* User-specific two-factor authentication settings.
*
* @type {Object}
*/
@tracked twoFaSettings = {};
/**
* Flag indicating whether system-wide two-factor authentication is enabled.
*
* @type {boolean}
*/
@tracked isSystemTwoFaEnabled = false;
/**
* Available two-factor authentication methods.
*
* @type {Array}
*/
@tracked methods = getTwoFaMethods();
/**
* Constructor method for the ConsoleAccountAuthController.
*
* @constructor
*/
constructor() {
super(...arguments);
this.loadSystemTwoFaConfig.perform();
this.loadUserTwoFaSettings.perform();
}
/**
* Validates the user's current password.
*
* @method validatePassword
* @param {Event} event - The event object triggering the action.
*/
@action validatePassword(event) {
event.preventDefault();
this.validatePasswordTask.perform();
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changeUserPasswordTask
* @param {Event} event - The event object triggering the action.
*/
@action changeUserPassword(event) {
event.preventDefault();
this.changeUserPasswordTask.perform();
}
/**
* Handles the event when two-factor authentication is toggled.
*
* @method onTwoFaToggled
* @param {boolean} enabled - Whether two-factor authentication is enabled or not.
*/
@action onTwoFaToggled(enabled) {
this.twoFaSettings = {
...this.twoFaSettings,
enabled,
};
}
/**
* Handles the event when a two-factor authentication method is selected.
*
* @method onTwoFaMethodSelected
* @param {string} method - The selected two-factor authentication method.
*/
@action onTwoFaMethodSelected(method) {
this.twoFaSettings = {
...this.twoFaSettings,
method,
};
}
/**
* Initiates the task to save user-specific two-factor authentication settings asynchronously.
*
* @method saveTwoFactorAuthSettings
*/
@action saveTwoFactorAuthSettings() {
this.saveUserTwoFaSettings.perform(this.twoFaSettings);
}
/**
* Initiates the task to save user-specific two-factor authentication settings asynchronously.
*
* @method saveUserTwoFaSettings
* @param {Object} twoFaSettings - User-specific two-factor authentication settings.
*/
@task *saveUserTwoFaSettings(twoFaSettings = {}) {
yield this.fetch
.post('users/two-fa', { twoFaSettings })
.then(() => {
this.notifications.success('2FA Settings saved successfully.');
})
.catch((error) => {
this.notifications.serverError(error);
});
}
/**
* Initiates the task to load user-specific two-factor authentication settings asynchronously.
*
* @method loadUserTwoFaSettings
*/
@task *loadUserTwoFaSettings() {
const twoFaSettings = yield this.fetch.get('users/two-fa');
if (twoFaSettings) {
this.isUserTwoFaEnabled = twoFaSettings.enabled;
this.twoFaSettings = twoFaSettings;
}
return twoFaSettings;
}
/**
* Initiates the task to load system-wide two-factor authentication configuration asynchronously.
*
* @method loadSystemTwoFaConfig
*/
@task *loadSystemTwoFaConfig() {
const twoFaConfig = yield this.fetch.get('two-fa/config');
if (twoFaConfig) {
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
this.twoFaConfig = twoFaConfig;
}
return twoFaConfig;
}
/**
* Initiates the task to validate the user's current password asynchronously.
*
* @method validatePasswordTask
*/
@task *validatePasswordTask() {
try {
yield this.fetch.post('users/validate-password', {
password: this.password,
password_confirmation: this.confirmPassword,
});
this.isPasswordValidated = true;
} catch (error) {
this.notifications.serverError(error, 'Invalid current password.');
}
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changeUserPasswordTask
*/
@task *changeUserPasswordTask() {
try {
yield this.fetch.post('users/change-password', {
password: this.newPassword,
password_confirmation: this.newConfirmPassword,
});
this.notifications.success('Password change successfully.');
} catch (error) {
this.notifications.error('Failed to change password');
}
}
}

View File

@@ -104,6 +104,16 @@ export default class ConsoleAdminBrandingController extends Controller {
.save()
.then(() => {
this.notifications.success('Branding settings saved.');
// if logo url is null
if (this.model.logo_url === null) {
this.model.set('logo_url', '/images/fleetbase-logo-svg.svg');
}
// if icon url is null
if (this.model.icon_url === null) {
this.model.set('icon_url', '/images/icon.png');
}
})
.finally(() => {
this.isLoading = false;

View File

@@ -0,0 +1,172 @@
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-decorators';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
/**
* Controller responsible for handling Two-Factor Authentication settings in the admin console.
*
* @class ConsoleAdminTwoFaSettingsController
* @extends Controller
*/
export default class ConsoleAdminTwoFaSettingsController extends Controller {
/**
* Service for handling data fetching.
*
* @type {fetch}
*/
@service fetch;
/**
* Service for displaying notifications.
*
* @type {notifications}
*/
@service notifications;
/**
* System-wide two-factor authentication configuration.
*
* @type {Object}
*/
@tracked twoFaConfig = {};
/**
* User-specific two-factor authentication settings.
*
* @type {Object}
*/
@tracked twoFaSettings = {};
/**
* Flag indicating whether system-wide two-factor authentication is enabled.
*
* @type {boolean}
*/
@tracked isSystemTwoFaEnabled = false;
/**
* Flag indicating whether 2FA enforcement is required.
*
* @type {boolean}
*/
@tracked isTwoFaEnforced = false;
/**
* Available two-factor authentication methods.
*
* @type {Array}
*/
@tracked methods = getTwoFaMethods();
/**
* Tracked property for the loading state
*
* @memberof ConsoleAdminTwoFaSettingsController
* @var {Boolean}
*/
@tracked isLoading = false;
/**
* Constructor method for the ConsoleAccountAuthController.
*
* @constructor
*/
constructor() {
super(...arguments);
this.loadSystemTwoFaConfig.perform();
}
/**
* Handles the event when two-factor authentication is toggled.
*
* @method onTwoFaToggled
* @param {boolean} enabled - Whether two-factor authentication is enabled or not.
*/
@action onTwoFaToggled(enabled) {
this.twoFaSettings = {
...this.twoFaSettings,
enabled,
};
}
/**
* Handles the event when a two-factor authentication method is selected.
*
* @method onTwoFaMethodSelected
* @param {string} method - The selected two-factor authentication method.
*/
@action onTwoFaMethodSelected(method) {
this.twoFaSettings = {
...this.twoFaSettings,
method,
};
}
/**
* Handles the event when two-factor authentication is toggled.
*
* @method onTwoFaToggled
* @param {boolean} enabled - Whether two-factor authentication is enforced or not.
*/
@action onTwoFaEnforceToggled(enforced) {
this.twoFaSettings = {
...this.twoFaSettings,
enforced,
};
}
/**
* Handles the event when 2FA enforcement is toggled.
*
* @method onTwoFaEnforceToggled
*/
/**
* Initiates the task to save user-specific two-factor authentication settings asynchronously.
*
* @method saveTwoFactorAuthSettings
*/
@action saveSettings() {
this.saveTwoFactorSettingsForAdmin.perform(this.twoFaSettings);
}
/**
* Initiates the task to load system-wide two-factor authentication configuration asynchronously.
*
* @method loadSystemTwoFaConfig
*/
@task *loadSystemTwoFaConfig() {
const twoFaSettings = yield this.fetch.get('two-fa/config').catch((error) => {
this.notifications.serverError(error);
});
if (twoFaSettings) {
this.twoFaSettings = twoFaSettings;
}
return twoFaSettings;
}
/**
* Initiates the task to save user-specific two-factor authentication settings asynchronously.
*
* @method saveTwoFactorSettingsForAdmin
* @param {Object} twoFaSettings - User-specific two-factor authentication settings.
*/
@task *saveTwoFactorSettingsForAdmin(twoFaSettings = {}) {
yield this.fetch
.post('two-fa/config', { twoFaSettings })
.then(() => {
this.notifications.success('2FA Settings saved for admin successfully.');
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
}

View File

@@ -0,0 +1,3 @@
import Controller from '@ember/controller';
export default class ConsoleSettingsAuthController extends Controller {}

View File

@@ -0,0 +1,184 @@
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-decorators';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
export default class ConsoleSettingsTwoFaController extends Controller {
/**
* Service for handling data fetching.
*
* @type {fetch}
*/
@service fetch;
/**
* Service for displaying notifications.
*
* @type {notifications}
*/
@service notifications;
/**
* System-wide two-factor authentication configuration.
*
* @type {Object}
*/
@tracked twoFaConfig = {};
/**
* User-specific two-factor authentication settings.
*
* @type {Object}
*/
@tracked twoFaSettings = {};
/**
* Flag indicating whether system-wide two-factor authentication is enabled.
*
* @type {boolean}
*/
@tracked isSystemTwoFaEnabled = false;
/**
* Flag indicating whether system-wide two-factor authentication is enabled.
*
* @type {boolean}
*/
@tracked isUserTwoFaEnabled = false;
/**
* Flag indicating whether 2FA enforcement is required.
*
* @type {boolean}
*/
@tracked isTwoFaEnforced = false;
/**
* Available two-factor authentication methods.
*
* @type {Array}
*/
@tracked methods = getTwoFaMethods();
/**
* Constructor method for the ConsoleAccountAuthController.
*
* @constructor
*/
constructor() {
super(...arguments);
this.loadSystemTwoFaConfig.perform();
this.loadCompanyTwoFaSettings.perform();
this.loadUserTwoFaSettings.perform();
}
/**
* Handles the event when two-factor authentication is toggled.
*
* @method onTwoFaToggled
* @param {boolean} enabled - Whether two-factor authentication is enabled or not.
*/
@action onTwoFaToggled(enabled) {
this.twoFaSettings = {
...this.twoFaSettings,
enabled,
};
}
/**
* Handles the event when a two-factor authentication method is selected.
*
* @method onTwoFaMethodSelected
* @param {string} method - The selected two-factor authentication method.
*/
@action onTwoFaMethodSelected(method) {
this.twoFaSettings = {
...this.twoFaSettings,
method,
};
}
/**
* Handles the event when two-factor authentication is toggled.
*
* @method onTwoFaToggled
* @param {boolean} enabled - Whether two-factor authentication is enforced or not.
*/
@action onTwoFaEnforceToggled(enforced) {
this.twoFaSettings = {
...this.twoFaSettings,
enforced,
};
}
/**
* Initiates the task to save user-specific two-factor authentication settings asynchronously.
*
* @method saveTwoFactor
*/
@action saveTwoFactor() {
this.saveTwoFactorSettingsForCompany.perform(this.twoFaSettings);
}
/**
* Initiates the task to load user-specific two-factor authentication settings asynchronously.
*
* @method loadUserTwoFaSettings
*/
@task *loadCompanyTwoFaSettings() {
const twoFaSettings = yield this.fetch.get('companies/two-fa');
if (twoFaSettings) {
this.twoFaSettings = twoFaSettings;
this.isTwoFaEnforced = twoFaSettings.enforced;
}
return twoFaSettings;
}
/**
* Initiates the task to load system-wide two-factor authentication configuration asynchronously.
*
* @method loadSystemTwoFaConfig
*/
@task *loadSystemTwoFaConfig() {
const twoFaConfig = yield this.fetch.get('two-fa/config');
if (twoFaConfig) {
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
this.twoFaConfig = twoFaConfig;
}
return twoFaConfig;
}
/**
* Initiates the task to load user-specific two-factor authentication settings asynchronously.
*
* @method loadUserTwoFaSettings
*/
@task *loadUserTwoFaSettings() {
const twoFaSettings = yield this.fetch.get('users/two-fa');
if (twoFaSettings) {
this.isUserTwoFaEnabled = twoFaSettings.enabled;
this.twoFaSettings = twoFaSettings;
}
return twoFaSettings;
}
/**
* Initiates the task to save user-specific two-factor authentication settings for the company asynchronously.
*
* @method saveTwoFactorSettingsForCompany
* @param {Object} twoFaSettings - User-specific two-factor authentication settings.
*/
@task *saveTwoFactorSettingsForCompany(twoFaSettings = {}) {
yield this.fetch
.post('companies/two-fa', { twoFaSettings })
.then(() => {
this.notifications.success('2FA Settings saved for organization successfully.');
})
.catch((error) => {
this.notifications.serverError(error);
});
}
}

View File

@@ -7,6 +7,7 @@ import { task } from 'ember-concurrency-decorators';
export default class InstallController extends Controller {
@service fetch;
@service notifications;
@service router;
@tracked error;
@tracked steps = [
{ task: 'createdb', name: 'Create Database', status: 'pending' },
@@ -46,7 +47,7 @@ export default class InstallController extends Controller {
if (isCompleted) {
this.notifications.success('Install completed successfully!');
return this.transitionToRoute('onboard');
return this.router.transitionTo('onboard');
}
}

View File

@@ -8,6 +8,7 @@ export default class InviteForUserController extends Controller {
@service session;
@service notifications;
@service modalsManager;
@service router;
@tracked code;
@tracked isLoading;
@@ -24,7 +25,7 @@ export default class InviteForUserController extends Controller {
this.isLoading = false;
return this.transitionToRoute('console').then(() => {
return this.router.transitionTo('console').then(() => {
if (response.needs_password && response.needs_password === true) {
this.setPassword();
}

View File

@@ -21,6 +21,13 @@ export default class OnboardIndexController extends Controller {
*/
@service session;
/**
* Inject the `router` service
*
* @memberof OnboardIndexController
*/
@service router;
/**
* Inject the `notifications` service
*
@@ -117,17 +124,18 @@ export default class OnboardIndexController extends Controller {
return this.fetch
.post('onboard/create-account', input)
.then((response) => {
if (response.status === 'success') {
this.session.isOnboarding().manuallyAuthenticate(response.token);
.then(({ status, skipVerification, token, session }) => {
if (status === 'success') {
if (skipVerification === true && token) {
// only manually authenticate if skip verification
this.session.isOnboarding().manuallyAuthenticate(token);
if (response.skipVerification === true) {
return this.transitionToRoute('console').then(() => {
return this.router.transitionTo('console').then(() => {
this.notifications.success('Welcome to Fleetbase!');
});
}
return this.transitionToRoute('onboard.verify-email', { queryParams: { hello: response.session } });
return this.router.transitionTo('onboard.verify-email', { queryParams: { hello: session } });
}
})
.catch((error) => {

View File

@@ -1,125 +1,7 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import AuthVerificationController from '../auth/verification';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { not } from '@ember/object/computed';
export default class OnboardVerifyEmailController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof OnboardIndexController
*/
@service fetch;
/**
* Inject the `notifications` service
*
* @memberof OnboardIndexController
*/
@service notifications;
/**
* Inject the `modalsManager` service
*
* @memberof OnboardIndexController
*/
@service modalsManager;
/**
* Inject the `currentUser` service
*
* @memberof OnboardIndexController
*/
@service currentUser;
/**
* The session paramerer.
*
* @memberof OnboardVerifyEmailController
*/
@tracked hello;
/**
* The loading state of the verification request.
*
* @memberof OnboardVerifyEmailController
*/
@tracked isLoading = false;
/**
* Validation state tracker.
*
* @memberof OnboardVerifyEmailController
*/
@tracked isReadyToSubmit = false;
/**
* The request timeout to trigger alternative verification options.
*
* @memberof OnboardVerifyEmailController
*/
@tracked waitTimeout = 800 * 60 * 2;
/**
* Determines if Fleetbase is still awaiting verification after a certain time.
*
* @memberof OnboardVerifyEmailController
*/
@tracked stillWaiting = false;
/**
* the input code.
*
* @memberof OnboardVerifyEmailController
*/
@tracked code;
/**
* The query param for the session token.
*
* @memberof OnboardVerifyEmailController
*/
@tracked queryParams = ['hello'];
/**
* The boolean opposite of `isReadyToSubmit`
*
* @memberof OnboardVerifyEmailController
*/
@not('isReadyToSubmit') isNotReadyToSubmit;
/**
* Creates an instance of OnboardVerifyEmailController.
* @memberof OnboardVerifyEmailController
*/
constructor() {
super(...arguments);
later(
this,
() => {
this.stillWaiting = true;
},
this.waitTimeout
);
}
/**
* Validates the input
*
* @param {InputEvent} { target: { value } }
* @memberof OnboardVerifyEmailController
*/
@action validateInput({ target: { value } }) {
if (value.length > 5) {
this.isReadyToSubmit = true;
} else {
this.isReadyToSubmit = false;
}
}
export default class OnboardVerifyEmailController extends AuthVerificationController {
/**
* Submits to verify code.
*
@@ -127,19 +9,24 @@ export default class OnboardVerifyEmailController extends Controller {
* @memberof OnboardVerifyEmailController
*/
@action verifyCode() {
const session = this.hello;
const code = this.code;
const { hello, code } = this;
this.isLoading = true;
return this.fetch
.post('onboard/verify-email', { session, code })
.then((response) => {
if (response.status === 'success') {
.post('onboard/verify-email', { session: hello, code })
.then(({ status, token }) => {
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
this.notifications.info('Welcome to Fleetbase!');
return this.transitionToRoute('console');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.session.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
}
})
.catch((error) => {
@@ -149,46 +36,4 @@ export default class OnboardVerifyEmailController extends Controller {
this.isLoading = false;
});
}
/**
* Action to resend verification code by SMS.
*
* @memberof OnboardVerifyEmailController
*/
@action resendBySms() {
this.modalsManager.show('modals/verify-by-sms', {
title: 'Verify Account by Phone',
acceptButtonText: 'Send',
phone: this.currentUser.phone,
confirm: (modal) => {
modal.startLoading();
const phone = modal.getOption('phone');
return this.fetch.post('onboard/send-verification-sms', { phone }).then(() => {
this.notifications.success('Verification code SMS sent!');
});
},
});
}
/**
* Action to resend verification code by email.
*
* @memberof OnboardVerifyEmailController
*/
@action resendEmail() {
this.modalsManager.show('modals/resend-verification-email', {
title: 'Resend Verification Code',
acceptButtonText: 'Send',
email: this.currentUser.email,
confirm: (modal) => {
modal.startLoading();
const email = modal.getOption('email');
return this.fetch.post('onboard/send-verification-email', { email }).then(() => {
this.notifications.success('Verification code email sent!');
});
},
});
}
}

View File

@@ -0,0 +1,21 @@
import config from 'ember-get-config';
export function initialize(owner) {
const universe = owner.lookup('service:universe');
if (universe) {
universe.registerOrganizationMenuItem(`v${config.version}`, {
index: 4,
route: null,
icon: 'code-branch',
iconSize: 'xs',
iconClass: 'mr-1.5',
wrapperClass: 'app-version-in-nav',
overwriteWrapperClass: true,
});
}
}
export default {
initialize,
};

View File

@@ -79,6 +79,11 @@ export default class UserModel extends Model {
@not('isEmailVerified') emailIsNotVerified;
@not('isPhoneVerified') phoneIsNotVerified;
/** @computed */
@computed('meta.two_factor_enabled') get isTwoFactorEnabled() {
return this.meta && this.meta.two_factor_enabled;
}
@computed('types') get typesList() {
const types = Array.from(this.types);
return types.join(', ');

View File

@@ -11,6 +11,8 @@ Router.map(function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password');
this.route('two-fa');
this.route('verification');
});
this.route('onboard', function () {
this.route('verify-email');
@@ -25,9 +27,11 @@ Router.map(function () {
this.route('notifications');
this.route('account', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('auth');
});
this.route('settings', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('two-fa');
});
this.route('virtual', { path: '/:slug/:view' });
this.route('admin', function () {
@@ -43,8 +47,29 @@ Router.map(function () {
});
this.route('branding');
this.route('notifications');
this.route('two-fa-settings');
this.route('virtual', { path: '/:slug/:view' });
});
this.mount('@fleetbase/dev-engine', {
as: 'developers',
path: 'developers'
});
this.mount('@fleetbase/fleetops-engine', {
as: 'fleet-ops',
path: 'fleet-ops'
});
this.mount('@fleetbase/iam-engine', {
as: 'iam',
path: 'iam'
});
this.mount('@fleetbase/storefront-engine', {
as: 'storefront',
path: 'storefront'
});
});
this.route('install');
});

View File

@@ -11,6 +11,7 @@ export default class ApplicationRoute extends Route {
@service urlSearchParams;
@service modalsManager;
@service intl;
@service router;
@tracked defaultTheme;
/**
@@ -27,11 +28,11 @@ export default class ApplicationRoute extends Route {
this.defaultTheme = defaultTheme;
if (shouldInstall) {
return this.transitionTo('install');
return this.router.transitionTo('install');
}
if (shouldOnboard) {
return this.transitionTo('onboard');
return this.router.transitionTo('onboard');
}
}
@@ -49,7 +50,7 @@ export default class ApplicationRoute extends Route {
const shift = this.urlSearchParams.get('shift');
if (isAuthenticated && shift) {
return this.transitionTo(pathToRoute(shift));
return this.router.transitionTo(pathToRoute(shift));
}
}

View File

@@ -0,0 +1,114 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class AuthTwoFaRoute extends Route {
/**
* Fetch service for making HTTP requests.
*
* @var {Service}
*/
@service fetch;
/**
* Notifications service for handling notifications.
*
* @var {Service}
*/
@service notifications;
/**
* Router service.
*
* @var {Service}
*/
@service router;
/**
* Session service for managing user sessions.
*
* @var {Service}
*/
@service session;
/**
* Query parameters for the route.
*
* @var {Object}
*/
queryParams = {
token: {
refreshModel: false,
replace: true,
},
clientToken: {
refreshModel: false,
replace: true,
},
};
/**
* Executes before the model is loaded, used for validating 2FA session with the server.
*
* @param {Object} transition - The transition object representing the route transition.
* @return {Promise} A promise that resolves if the 2FA session is valid, and rejects with an error otherwise.
*/
beforeModel(transition) {
// validate 2fa session with server
let { token, clientToken } = transition.to.queryParams;
return this.session.store.restore().then(({ identity }) => {
if (!identity) {
this.notifications.error('2FA failed to initialize.');
return this.router.transitionTo('auth.login');
}
return this.fetch
.post('two-fa/validate', { token, identity, clientToken })
.then(({ clientToken, expired }) => {
// handle when code expired
if (expired === true) {
return this.invalidateTwoFaSession(token, identity);
}
// clear session data after validated 2fa session
this.session.store.persist({
identity,
token,
clientToken,
});
})
.catch((error) => {
this.notifications.serverError(error);
return this.router.transitionTo('auth.login');
});
});
}
/**
* Sets up the controller, including client token and session expiration details.
*
* @param {Object} controller - The controller for the route.
*/
setupController(controller) {
super.setupController(...arguments);
this.session.store.restore().then(({ clientToken, identity }) => {
controller.clientToken = clientToken;
controller.identity = identity;
controller.twoFactorSessionExpiresAfter = controller.getExpirationDateFromClientToken(clientToken);
controller.countdownReady = true;
});
}
invalidateTwoFaSession(token, identity) {
this.notifications.error('2FA authentication session has expired.');
return this.fetch
.post('two-fa/invalidate', {
token,
identity,
})
.then(() => {
return this.router.transitionTo('auth.login');
});
}
}

View File

@@ -0,0 +1,33 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class AuthVerificationRoute extends Route {
@service session;
@service fetch;
@service router;
queryParams = {
token: {
refreshModel: false,
replace: true,
},
};
beforeModel(transition) {
let { token } = transition.to.queryParams;
return this.session.store.restore().then(({ email }) => {
return this.fetch.post('auth/validate-verification-session', { email, token }).then(({ valid }) => {
if (!valid) {
return this.router.transitionTo('auth.login');
}
});
});
}
async setupController(controller) {
super.setupController(...arguments);
let { email } = await this.session.store.restore();
controller.email = email;
}
}

View File

@@ -25,6 +25,13 @@ export default class ConsoleRoute extends Route {
*/
@service session;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `currentUser` service
*
@@ -72,6 +79,12 @@ export default class ConsoleRoute extends Route {
@action setupController(controller, model) {
super.setupController(controller, model);
// Get and set user locale
this.fetch.get('users/locale').then(({ locale }) => {
this.intl.setLocale(locale);
});
// Get user organizations
this.fetch.get('auth/organizations').then((organizations) => {
this.currentUser.setOption('organizations', organizations);
controller.organizations = organizations;

View File

@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class ConsoleAccountAuthRoute extends Route {}

View File

@@ -4,11 +4,12 @@ import { inject as service } from '@ember/service';
export default class ConsoleAdminRoute extends Route {
@service currentUser;
@service notifications;
@service router;
beforeModel() {
// USER MUST BE ADMIN
if (!this.currentUser.user.is_admin) {
return this.transitionTo('console').then(() => {
return this.router.transitionTo('console').then(() => {
this.notifications.error('You do not have authorization to access admin!');
});
}

View File

@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class ConsoleAdminTwoFaSettingsRoute extends Route {}

View File

@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class ConsoleSettingsAuthRoute extends Route {}

View File

@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class ConsoleSettingsTwoFaRoute extends Route {}

View File

@@ -0,0 +1,4 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
export default class TwoFaSettingsSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {}

View File

@@ -1,4 +1,5 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'inter-ui/inter.css';
@import 'inter-ui/inter.css';
@import 'console.css';

View File

@@ -0,0 +1,23 @@
.two-fa-enforcement-alert svg.fa-triangle-exclamation {
font-size: 2.25rem;
padding-right: 0.5rem;
color: rgb(202 138 4);
}
.two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning,
body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning {
background-color: rgb(202 138 4);
border-color: rgb(161 98 7);
color: rgb(254 249 195);
cursor: default;
}
.app-version-in-nav {
display: flex;
align-items: center;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1rem;
padding-left: 1rem;
padding-top: 0.2rem;
}

View File

@@ -2,7 +2,7 @@
<div class="mb-4">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{if this.isSent "Almost done!" "Forgot your password?"}}
{{if this.isSent (t "auth.forgot-password.is-sent.title") (t "auth.forgot-password.not-sent.title")}}
</h2>
</div>
@@ -12,7 +12,7 @@
<FaIcon @icon="check-circle" @size="lg" class="text-green-900 mr-4" />
</div>
<p class="flex-1 text-sm text-green-900 dark:text-green-900">
<strong>Check your email!</strong><br> We've sent you a magic link to your email which will allow you to reset your password. The link expires in 15 minutes.
{{t "auth.forgot-password.is-sent.message" htmlSafe=true}}
</p>
</div>
{{else}}
@@ -21,23 +21,23 @@
<FaIcon @icon="info-circle" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
<strong>Don't worry, we've got your back.</strong><br> Enter the email you use to login to {{t "app.name"}} and we will send you a secure link to reset your password.
{{t "auth.forgot-password.not-sent.message" htmlSafe=true appName=(t "app.name")}}
</p>
</div>
<form class="space-y-6" {{on "submit" this.sendSecureLink}}>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-50">
Your email address
{{t "auth.forgot-password.form.email-label"}}
</label>
<div class="mt-2">
<Input @value={{this.email}} @type="email" id="email" name="email" required class="form-input form-input-lg w-full" placeholder="Your email" />
<Input @value={{this.email}} @type="email" id="email" name="email" required class="form-input form-input-lg w-full" placeholder={{t "auth.forgot-password.form.email-label"}} />
</div>
</div>
<div class="flex flex-row space-x-2">
<Button @icon="magic" @type="primary" @buttonType="submit" @text="OK, Send me a magic link!" @onClick={{this.sendSecureLink}} @isLoading={{this.isLoading}} />
<Button @buttonType="button" @text="Nevermind" @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />
<Button @icon="magic" @type="primary" @buttonType="submit" @text={{t "auth.forgot-password.form.submit-button"}} @onClick={{this.sendSecureLink}} @isLoading={{this.isLoading}} />
<Button @buttonType="button" @text={{t "auth.forgot-password.form.nevermind-button"}} @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />
</div>
</form>
{{/if}}

View File

@@ -3,7 +3,7 @@
<LogoIcon @url={{@brand.icon_url}} @size="12" class="mx-auto" />
</div>
<h2 class="mt-6 mb-3 text-3xl font-extrabold leading-9 text-center text-gray-900 dark:text-gray-100">
Sign in to your account
{{t "auth.login.title"}}
</h2>
</div>
@@ -14,10 +14,10 @@
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-900 mr-4" />
</div>
<p class="flex-1 text-sm text-yellow-900 dark:yellow-red-900">
<strong>Forgot your password?</strong><br> Click the button below to reset your password.
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
</p>
</div>
<Button @text="Ok, help me reset!" @type="warning" @onClick={{this.forgotPassword}} />
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="warning" @onClick={{this.forgotPassword}} />
</div>
{{/if}}
@@ -25,10 +25,30 @@
<input type="hidden" name="remember" value="true" />
<div class="rounded-md shadow-sm">
<div>
<Input @value={{this.email}} aria-label="Email address" name="email" @type="email" autocomplete="username" required class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900" placeholder="Email address" disabled={{this.isLoading}} />
<Input
@value={{this.identity}}
aria-label={{t "auth.login.form.email-label"}}
name="email"
@type="email"
autocomplete="username"
required
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
placeholder={{t "auth.login.form.email-label"}}
disabled={{this.isLoading}}
/>
</div>
<div class="-mt-px">
<Input @value={{this.password}} aria-label="Password" name="password" @type="password" autocomplete="current-password" required class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900" placeholder="Password" disabled={{this.isLoading}} />
<Input
@value={{this.password}}
aria-label={{t "auth.login.form.password-label"}}
name="password"
@type="password"
autocomplete="current-password"
required
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
placeholder={{t "auth.login.form.password-label"}}
disabled={{this.isLoading}}
/>
</div>
</div>
@@ -36,22 +56,27 @@
<div class="flex items-center">
<Input id="rememberMe" @type="checkbox" @checked={{this.rememberMe}} disabled={{this.isLoading}} class="w-4 h-4 transition duration-150 ease-in-out form-checkbox text-sky-500" />
<label for="rememberMe" class="block ml-2 text-sm leading-5 text-gray-900 dark:text-gray-100">
Remember me
{{t "auth.login.form.remember-me-label"}}
</label>
</div>
<div class="text-sm leading-5">
<a href="javascript:;" {{on "click" this.forgotPassword}} disabled={{this.isLoading}} class="font-medium transition duration-150 ease-in-out text-sky-600 hover:text-sky-500 focus:outline-none focus:underline">
Forgot your password?
<a
href="javascript:;"
{{on "click" this.forgotPassword}}
disabled={{this.isLoading}}
class="font-medium transition duration-150 ease-in-out text-sky-600 hover:text-sky-500 focus:outline-none focus:underline"
>
{{t "auth.login.form.forgot-password-label"}}
</a>
</div>
</div>
<div class="mt-6">
<Button @buttonType="submit" @type="primary" @text="Sign In" @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
<Button @buttonType="submit" @type="primary" @text={{t "auth.login.form.sign-in-button"}} @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
</div>
<div class="mt-3">
<Button @text="Create a new Account" @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
<Button @text={{t "auth.login.form.create-account-button"}} @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
</div>
</form>

View File

@@ -2,18 +2,18 @@
<div class="mb-4">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
Reset your password
{{t "auth.reset-password.title"}}
</h2>
</div>
<form class="space-y-6" {{on "submit" this.resetPassword}}>
<InputGroup @name="Your reset code" @value={{this.code}} @inputClass="form-input-lg" @helpText="The verification code you received in your email." />
<InputGroup @name="New Password" @value={{this.password}} @type="password" @inputClass="form-input-lg" @helpText="Enter a password at-least 6 characters to continue." />
<InputGroup @name="Confirm new Password" @value={{this.password_confirmation}} @type="password" @inputClass="form-input-lg" @helpText="Enter a password at-least 6 characters to continue." />
<InputGroup @name={{t "auth.reset-password.form.code.label"}} @value={{this.code}} @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.code.help-text"}} />
<InputGroup @name={{t "auth.reset-password.form.password.label"}} @value={{this.password}} @type="password" @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.password.help-text"}} />
<InputGroup @name={{t "auth.reset-password.form.confirm-password.label"}} @value={{this.password_confirmation}} @type="password" @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.confirm-password.help-text"}} />
<div class="flex space-x-2">
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text="Reset Password" @onClick={{this.resetPassword}} @isLoading={{this.isLoading}} />
<Button @size="lg" @buttonType="button" @text="Back" @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text={{t "auth.reset-password.form.submit-button"}} @onClick={{this.resetPassword}} @isLoading={{this.isLoading}} />
<Button @size="lg" @buttonType="button" @text={{t "auth.reset-password.form.back-button"}} @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />
</div>
</form>
</div>

View File

@@ -0,0 +1,47 @@
<div class="mb-8 text-center">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<h2 class="text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{if this.isSent "Verification Code"}}
</h2>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-green-200">
<div>
<FaIcon @icon="check-circle" @size="lg" class="text-green-900 mr-4" />
</div>
<p class="flex-1 text-sm text-green-900 dark:text-green-900">
<strong>Check your {{this.selectedMethod}}</strong><br />
We've sent you a verification code. Enter the code below to complete the login process.
</p>
</div>
<form class="mt-8" {{on "submit" this.verifyCode}}>
<div class="flex items-center justify-between my-6">
<OtpInput @onInputCompleted={{this.handleOtpInput}} />
</div>
<div id="otp-countdown-container" class="otp-countdown-container flex items-center justify-center min-h-12">
{{#if this.countdownReady}}
<Countdown @expiry={{this.twoFactorSessionExpiresAfter}} @countdownClass="text-lg" @onCountdownEnd={{this.handleCodeExpired}} />
{{/if}}
{{#if this.isCodeExpired}}
<InfoBlock>
<div>Your 2FA authentication code has expired. You can request another code if you need more time.</div>
<Button @type="primary" @wrapperClass="mt-2" @text="Resend Code" @icon="arrow-rotate-right" @onClick={{this.resendCode}} />
</InfoBlock>
{{/if}}
</div>
<div class="mt-4">
<Button @buttonType="submit" @type="primary" @text="Verify Code" @icon="check-circle" @wrapperClass="btn-block" @isLoading={{this.isLoading}} />
</div>
<div class="text-center flex flex-row items-center justify-center space-x-4 mt-3.5">
<a href="#" class="text-sm text-blue-500 hover:underline inline-block" {{on "click" this.resendCode}}>
Resend Code
</a>
<a href="#" class="text-sm text-danger hover:underline inline-block" {{on "click" this.cancelTwoFactor}}>
Cancel Two-Factor
</a>
</div>
</form>

View File

@@ -0,0 +1,46 @@
{{page-title "Account Verification"}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-8">
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
Verify your email address
</h2>
</div>
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
<strong>Almost done!</strong><br> Check your email for a verification code.
</p>
</div>
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
<InputGroup @type="tel" @name="Verification Code" @value={{this.code}} @helpText="Enter the verification code you received via email." @inputClass="input-lg" {{on "input" this.validateInput}} />
<div>
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
</div>
{{#if this.stillWaiting}}
<div class="bg-yellow-50 rounded shadow-sm border-l-4 border-yellow-400 px-4 py-2">
<div class="flex">
<div class="flex-shrink-0">
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
</div>
<div class="ml-3 flex items-center">
<span class="text-lg font-extrabold text-yellow-800">Didn't receive an email yet?</span>
</div>
</div>
<div class="py-3">
<p class="text-yellow-700">Use alternaitve options below to verify your account.</p>
<div class="flex items-center mt-3">
<Button @type="default" class="mr-2" @onClick={{this.resendEmail}}>Resend Email</Button>
<Button @type="default" @onClick={{this.resendBySms}}>Send by SMS</Button>
</div>
</div>
</div>
{{/if}}
</form>
</div>

View File

@@ -1,10 +1,11 @@
{{page-title "Console"}}
{{page-title (t "app.name")}}
<Layout::Container>
<Layout::Header @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.universe.headerMenuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} />
<Layout::Header @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.universe.headerMenuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} @showSidebarToggle={{true}} @sidebarToggleEnabled={{this.sidebarToggleEnabled}} @onSidebarToggle={{this.onSidebarToggle}} />
<Layout::Main>
<Layout::Sidebar @onSetup={{this.setSidebarContext}}>
<div class="next-sidebar-content-inner">
<div role="menu" id="sidebar-menu-items"></div>
<div role="menu" id="sidebar-menu-items">
</div>
</div>
</Layout::Sidebar>
<Layout::Section>
@@ -12,4 +13,9 @@
</Layout::Section>
</Layout::Main>
<Layout::MobileNavbar @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.universe.headerMenuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} />
</Layout::Container>
</Layout::Container>
{{!-- Add Locale Selector to Header --}}
<EmberWormhole @to="view-header-actions">
<LocaleSelector class="mr-0.5" />
</EmberWormhole>

View File

@@ -1,7 +1,8 @@
{{page-title "Account"}}
<EmberWormhole @to="sidebar-menu-items">
<Layout::Sidebar::Panel @open={{true}} @title="Account">
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.account"}}>
<Layout::Sidebar::Item @route="console.account.index" @icon="user">Profile</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.account.auth" @icon="key">Auth</Layout::Sidebar::Item>
{{#each this.universe.accountMenuItems as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}

View File

@@ -0,0 +1,57 @@
{{page-title "Account Auth"}}
<Layout::Section::Header @title="Account Auth" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
{{#if this.isPasswordValidated}}
<form id="change-password-form" aria-label="change-password" {{on "submit" this.changeUserPassword}}>
<legend class="mb-3">Change Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Enter new Password" @type="password" @value={{this.newPassword}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.newConfirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Confirm & Change Password" @icon="save" {{on "click" this.changeUserPassword}} />
</form>
{{else}}
<form id="validate-password-form" aria-label="validate-password" {{on "submit" this.validatePassword}}>
<legend class="mb-3">Validate Current Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Password" @type="password" @value={{this.password}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.confirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Continue" @icon="check" {{on "click" this.validatePassword}} />
</form>
{{/if}}
</ContentPanel>
{{#if this.isSystemTwoFaEnabled}}
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="mb-3">
{{#if this.loadUserTwoFaSettings.isIdle}}
<TwoFaSettings
@twoFaMethods={{this.methods}}
@twoFaSettings={{this.twoFaSettings}}
@onTwoFaToggled={{this.onTwoFaToggled}}
@onTwoFaMethodSelected={{this.onTwoFaMethodSelected}}
/>
{{else}}
<div class="flex items-center justify-center p-4">
<Spinner @loadingMessage="Loading User 2FA Settings..." @wrapperClass="flex flex-row" @iconClass="mr-2" />
</div>
{{/if}}
</div>
<Button
@type="primary"
@buttonType="submit"
@text="Save 2FA Settings"
@icon="save"
@onClick={{this.saveTwoFactorAuthSettings}}
@isLoading={{this.saveUserTwoFaSettings.isRunning}}
/>
</ContentPanel>
{{/if}}
</div>
</div>
</Layout::Section::Body>

View File

@@ -1,25 +1,25 @@
<Layout::Section::Header @title="Profile" />
<Layout::Section::Header @title={{t "common.profile"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="max-w-3xl my-10 mx-auto">
<ContentPanel @title="Your Profile" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title={{t "common.your-profile"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form class="flex flex-col md:flex-row" {{on "submit" this.saveProfile}}>
<div class="w-32 flex flex-col justify-center mb-6 mr-6">
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md" />
<FileUpload @name="photos" @accept="image/*" @onFileAdded={{this.uploadNewPhoto}} @labelClass="flex flex-row items-center justify-center" as |queue|>
<FileUpload @name={{t "console.account.index.photos"}} @accept="image/*" @onFileAdded={{this.uploadNewPhoto}} @labelClass="flex flex-row items-center justify-center" as |queue|>
<a tabindex={{0}} class="flex items-center px-0 mt-2 text-xs no-underline truncate btn btn-sm btn-default" disabled={{queue.files.length}}>
{{#if queue.files.length}}
<div class="mr-1.5">
<Spinner />
</div>
<span>
Uploading...
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
Upload new
<span>
{{t "console.account.index.upload-new"}}
</span>
{{/if}}
</a>
@@ -27,15 +27,15 @@
</div>
<div class="flex-1">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2 text-xs dark:text-gray-100">
<InputGroup @name="Name" @value={{this.user.name}} />
<InputGroup @name="Email" @type="email" @value={{this.user.email}} />
<InputGroup @name="Your phone number" @helpText="Your phone number.">
<InputGroup @name={{t "common.name"}} @value={{this.user.name}} />
<InputGroup @name={{t "common.email"}} @type="email" @value={{this.user.email}} />
<InputGroup @name={{t "common.phone"}} @helpText={{t "console.account.index.phone"}}>
<PhoneInput @value={{this.user.phone}} @onInput={{fn (mut this.user.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name="Date of Birth" @type="date" @value={{this.user.date_of_birth}} />
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
</div>
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="Save Changes" @onClick={{this.saveProfile}} @isLoading={{this.isLoading}} />
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveProfile}} @isLoading={{this.isLoading}} />
</div>
</div>
</form>

View File

@@ -1,9 +1,10 @@
{{page-title "Admin"}}
<EmberWormhole @to="sidebar-menu-items">
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">Overview</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">Branding</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.notifications" @icon="bell">Notifications</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">{{t "common.overview"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "common.branding"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "common.2fa-config"}}</Layout::Sidebar::Item>
{{#each this.universe.adminMenuItems as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
@@ -15,12 +16,12 @@
</Layout::Sidebar::Panel>
{{/each}}
<Layout::Sidebar::Panel @open={{true}} @title="System Config">
<Layout::Sidebar::Item @route="console.admin.config.services" @icon="bell-concierge">Services</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.mail" @icon="envelope">Mail</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.filesystem" @icon="hard-drive">Filesystem</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.queue" @icon="layer-group">Queue</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.socket" @icon="plug">Socket</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.notification-channels" @icon="tower-broadcast">Notification Channels</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.services" @icon="bell-concierge">{{t "common.services"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.mail" @icon="envelope">{{t "common.mail"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.filesystem" @icon="hard-drive">{{t "common.filesystem"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.queue" @icon="layer-group">{{t "common.queue"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.socket" @icon="plug">{{t "common.socket"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.notification-channels" @icon="tower-broadcast">{{t "common.notification-channels"}}</Layout::Sidebar::Item>
</Layout::Sidebar::Panel>
</EmberWormhole>

View File

@@ -1,62 +1,62 @@
{{page-title "Branding"}}
<Layout::Section::Header @title="Branding">
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{this.save}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
{{page-title (t "console.admin.branding.title")}}
<Layout::Section::Header @title={{t "console.admin.branding.title"}}>
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.save}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
</Layout::Section::Header>
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="max-w-3xl my-10 mx-auto space-y-">
<ContentPanel @title="Branding" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title={{t "console.admin.branding.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form class="flex flex-col" {{on "submit" this.save}}>
<div class="input-group">
<label>Icon</label>
<label>{{t "console.admin.branding.icon-text"}}</label>
<div class="flex flex-row items-center space-x-2">
<Image src={{@model.icon_url}} @fallbackSrc="/images/icon.png" alt={{t "app.name"}} width="32" height="32" class="w-8 h-8 shadow-sm" />
<FileUpload @name="icon" @accept="image/*" @onFileAdded={{this.uploadIcon}} @labelClass="flex flex-row items-center justify-center mb-0i" as |queue|>
<FileUpload @name={{t "console.admin.branding.icon-text"}} @accept="image/*" @onFileAdded={{this.uploadIcon}} @labelClass="flex flex-row items-center justify-center mb-0i" as |queue|>
<a tabindex={{0}} class="flex items-center px-0 text-xs no-underline truncate btn btn-sm btn-default" disabled={{queue.files.length}}>
{{#if queue.files.length}}
<div class="mr-1.5">
<Spinner />
</div>
<span>
Uploading...
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
Upload new
{{t "console.admin.branding.upload-new"}}
</span>
{{/if}}
</a>
</FileUpload>
</div>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetIcon}}>Reset to default</a>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetIcon}}>{{t "console.admin.branding.reset-default"}}</a>
</div>
<div class="input-group">
<label>Logo</label>
<label>{{t "console.admin.branding.logo-text"}}</label>
<div class="flex flex-row items-center space-x-2">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14" />
<FileUpload @name="logo" @accept="image/*" @onFileAdded={{this.uploadLogo}} @labelClass="flex flex-row items-center justify-center mb-0i" as |queue|>
<FileUpload @name={{t "console.admin.branding.logo-text"}} @accept="image/*" @onFileAdded={{this.uploadLogo}} @labelClass="flex flex-row items-center justify-center mb-0i" as |queue|>
<a tabindex={{0}} class="flex items-center px-0 text-xs no-underline truncate btn btn-sm btn-default" disabled={{queue.files.length}}>
{{#if queue.files.length}}
<div class="mr-1.5">
<Spinner />
</div>
<span>
Uploading...
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
Upload new
{{t "console.admin.branding.upload-new"}}
</span>
{{/if}}
</a>
</FileUpload>
</div>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetLogo}}>Reset to default</a>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetLogo}}>{{t "console.admin.branding.reset-default"}}</a>
</div>
<InputGroup @name="Default Theme">
<InputGroup @name={{t "console.admin.branding.theme"}}>
<Select @value={{@model.default_theme}} @onSelect={{this.setTheme}} @options={{this.themeOptions}} />
</InputGroup>
</form>

View File

@@ -1,5 +1,5 @@
{{page-title "Database Configuration"}}
<Layout::Section::Header @title="Database Configuration" />
<Layout::Section::Header @title={{t "console.admin.config.database.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>

View File

@@ -1,5 +1,5 @@
{{page-title "Filesystem Configuration"}}
<Layout::Section::Header @title="Filesystem Configuration" />
<Layout::Section::Header @title={{t "console.admin.config.filesystem.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>

View File

@@ -1,5 +1,5 @@
{{page-title "Mail Configuration"}}
<Layout::Section::Header @title="Mail Configuration" />
<Layout::Section::Header @title={{t "console.admin.config.mail.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>

View File

@@ -1,5 +1,5 @@
{{page-title "Notification Channels Configuration"}}
<Layout::Section::Header @title="Notification Channels Configuration" />
<Layout::Section::Header @title={{t "console.admin.config.notification-channels.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>

View File

@@ -1,5 +1,5 @@
{{page-title "Queue Configuration"}}
<Layout::Section::Header @title="Queue Configuration" />
<Layout::Section::Header @title={{t "console.admin.config.queue.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>

View File

@@ -1,5 +1,5 @@
{{page-title "Services Configuration"}}
<Layout::Section::Header @title="Services Configuration" />
<Layout::Section::Header @title={{t "console.admin.config.services.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 900}}>

View File

@@ -1,5 +1,5 @@
{{page-title "Socket Configuration"}}
<Layout::Section::Header @title="Socket Configuration" />
<Layout::Section::Header @title={{t "console.admin.config.socket.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 900}}>

View File

@@ -1,13 +1,13 @@
{{page-title "Overview"}}
<Layout::Section::Header @title="Overview" />
<Layout::Section::Header @title={{t "common.overview"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="max-w-3xl my-10 mx-auto space-y-6">
<div class="grid grid-cols-3 xs:grid-cols-1 gap-4">
<StatWidget @title="Total Users" @value={{@model.total_users}} />
<StatWidget @title="Total Organizations" @value={{@model.total_organizations}} />
<StatWidget @title="Total Transactions" @value={{@model.total_transactions}} />
<StatWidget @title={{t "console.admin.index.total-users"}} @value={{@model.total_users}} />
<StatWidget @title={{t "console.admin.index.total-organizations"}} @value={{@model.total_organizations}} />
<StatWidget @title={{t "console.admin.index.total-transactions"}} @value={{@model.total_transactions}} />
</div>
</div>
</div>

View File

@@ -1,13 +1,13 @@
{{page-title "Notifications"}}
<Layout::Section::Header @title="Notifications">
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
<Layout::Section::Header @title={{t "console.admin.notifications.title"}}>
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
</Layout::Section::Header>
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 1200}}>
<div class="max-w-3xl my-10 mx-auto space-y-4">
{{#each-in this.groupedNotifications as |groupName notifications|}}
<ContentPanel @title={{concat (smart-humanize groupName) " Notification Settings"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title={{concat (smart-humanize groupName) (t "console.admin.notifications.notification-settings") }} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
{{#each notifications as |notification|}}
<InputGroup @name={{notification.name}} @helpText={{notification.description}}>
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">

View File

@@ -0,0 +1,28 @@
{{page-title "2FA Config"}}
<Layout::Section::Header @title="2FA Config">
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
</Layout::Section::Header>
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 1200}}>
<div class="max-w-3xl my-10 mx-auto space-y-4">
<ContentPanel @title="2FA Config" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
{{#if this.loadSystemTwoFaConfig.isIdle}}
<TwoFaSettings
@showEnforceOption={{true}}
@twoFaMethods={{this.methods}}
@twoFaSettings={{this.twoFaSettings}}
@onTwoFaToggled={{this.onTwoFaToggled}}
@onTwoFaMethodSelected={{this.onTwoFaMethodSelected}}
@onTwoFaEnforcedToggled={{this.onTwoFaEnforceToggled}}
@isLoading={{this.isLoading}}
/>
{{else}}
<div class="flex items-center justify-center p-4">
<Spinner @loadingMessage="Loading 2FA Config..." @wrapperClass="flex flex-row" @iconClass="mr-2" />
</div>
{{/if}}
</ContentPanel>
</div>
</div>
</Layout::Section::Body>

View File

@@ -3,9 +3,9 @@
<div class="container mx-auto h-screen space-y-4">
<div class="flex flex-col items-center justify-center pt-14 px-40">
<FaIcon @icon="shapes" @size="4x" class="mb-6 text-blue-500" />
<h1 class="dark:text-gray-100 text-black text-4xl font-bold mb-4">Extensions are coming soon!</h1>
<h1 class="dark:text-gray-100 text-black text-4xl font-bold mb-4">{{t "console.extensions.title"}}</h1>
<p class="dark:text-gray-300 text-black text-lg">
Please check back in the upcoming versions as we prepare to launch the Extensions repository and marketplace.
{{t "console.extensions.message"}}
</p>
</div>
</div>

View File

@@ -1,6 +1,7 @@
{{page-title "Dashboard"}}
<Layout::Section::Body class="overflow-y-scroll h-full pt-6">
<div class="container mx-auto h-screen space-y-4" {{increase-height-by 300}}>
<div class="console-home-container mx-auto h-screen space-y-4" {{increase-height-by 300}}>
<TwoFaEnforcementAlert />
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<GithubCard class="lg:col-span-4" />
<FleetbaseBlog class="lg:col-span-8" />

View File

@@ -1,7 +1,7 @@
<Layout::Section::Header @title="Notifications">
<Button @icon="check-square" @type="default" @text="Select All" {{on "click" this.selectAll}} class="mr-2" />
<Button @icon="envelope" @type="primary" @text="Mark as Read" {{on "click" this.read}} class="mr-2" />
<Button @icon="trash" @type="danger" @text="Delete" {{on "click" this.delete}} />
<Layout::Section::Header @title={{t "common.notifications"}}>
<Button @icon="check-square" @type="default" @text={{t "console.notifications.select-all"}} {{on "click" this.selectAll}} class="mr-2" />
<Button @icon="envelope" @type="primary" @text={{t "console.notifications.mark-as-read"}} {{on "click" this.read}} class="mr-2" />
<Button @icon="trash" @type="danger" @text={{t "common.delete"}} {{on "click" this.delete}} />
</Layout::Section::Header>
<Layout::Section::Body class="h-full w-full">
@@ -26,7 +26,7 @@
<h1 class="text-sm font-semibold antialiased leading-4">{{notification.data.subject}}</h1>
<div class="text-xs antialiased text-gray-900 dark:text-gray-200">- {{notification.data.message}}</div>
</div>
<div class="text-gray-300 text-xs antialiased mt-1">Received: {{notification.createdAgo}}</div>
<div class="text-gray-300 text-xs antialiased mt-1">{{t "console.notifications.received"}} {{notification.createdAgo}}</div>
</div>
</a>
<div>
@@ -37,7 +37,7 @@
</div>
{{else}}
<div class="flex items-center justify-center h-full w-full">
<p class="text-base text-gray-800 dark:text-gray-300 italic">No notifications to display.</p>
<p class="text-base text-gray-800 dark:text-gray-300 italic">{{t "console.notifications.message"}}</p>
</div>
{{/each}}
</div>

View File

@@ -1,18 +1,28 @@
{{page-title "Settings"}}
{{page-title (t "common.settings")}}
<EmberWormhole @to="sidebar-menu-items">
<Layout::Sidebar::Panel @open={{true}} @title="Settings">
<Layout::Sidebar::Item @route="console.settings.index" @icon="cog">Organization</Layout::Sidebar::Item>
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.settings"}}>
<Layout::Sidebar::Item @route="console.settings.index" @icon="cog">{{t "common.organization"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.settings.two-fa" @icon="shield-halved">{{t "common.two-factor"}}</Layout::Sidebar::Item>
{{#each this.universe.settingsMenuItems as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item
@onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}}
@item={{menuItem}}
@icon={{menuItem.icon}}
>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
</Layout::Sidebar::Panel>
{{#each this.universe.settingsMenuPanels as |menuPanel|}}
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
{{#each menuPanel.items as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item
@onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}}
@item={{menuItem}}
@icon={{menuItem.icon}}
>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
</Layout::Sidebar::Panel>
{{/each}}
</EmberWormhole>
{{outlet}}

View File

@@ -1,40 +1,40 @@
<Layout::Section::Header @title="Organization Settings" />
<Layout::Section::Header @title={{t "console.settings.index.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="Organization Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form {{on "submit" this.saveSettings}}>
<InputGroup @name="Organization name" @value={{@model.name}} />
<InputGroup @name="Organization description" @value={{@model.description}} />
<InputGroup @name="Organization phone number">
<InputGroup @name={{t "console.settings.index.organization-name"}} @value={{@model.name}} />
<InputGroup @name={{t "console.settings.index.organization-description"}} @value={{@model.description}} />
<InputGroup @name={{t "console.settings.index.organization-phone"}}>
<PhoneInput @value={{@model.phone}} @onInput={{fn (mut @model.phone)}} class="form-input w-full" />
</InputGroup>
<InputGroup @name="Organization currency">
<InputGroup @name={{t "console.settings.index.organization-currency"}}>
<CurrencySelect @value={{@model.currency}} @onSelect={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
</InputGroup>
<InputGroup @name="Organization ID" @value={{@model.public_id}} @disabled={{true}} />
<InputGroup @name={{t "console.settings.index.organization-id"}} @value={{@model.public_id}} @disabled={{true}} />
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="Save Changes" @isLoading={{this.isLoading}} />
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="{{t "common.save-button-text"}}" @isLoading={{this.isLoading}} />
</div>
</form>
</ContentPanel>
<ContentPanel @title="Organization Branding" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Logo" @helpText="Logo for your organization.">
<ContentPanel @title={{t "console.settings.index.organization-branding"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name={{t "console.settings.index.logo"}} @helpText={{t "console.settings.index.logo-help-text"}}>
<div class="flex flex-row items-center">
<Image src={{@model.logo_url}} @fallbackSrc={{config "defaultValues.placeholderImage"}} alt={{concat @model.name " logo"}} class="h-20 w-64 border dark:border-gray-900 rounded-md mr-4" />
<FileUpload @name="logo" @accept="image/*" @onFileAdded={{fn this.uploadFile "logo"}} as |queue|>
<FileUpload @name={{t "console.settings.index.logo"}} @accept="image/*" @onFileAdded={{fn this.uploadFile "logo"}} as |queue|>
<a tabindex={{0}} class="flex items-center px-0 mt-2 text-xs no-underline truncate btn btn-sm btn-default">
{{#if queue.files.length}}
<Spinner class="mr-1" />
<span>
Uploading...
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="image" class="mr-1" />
<span>
Upload new logo
{{t "console.settings.index.upload-new-logo"}}
</span>
{{/if}}
</a>
@@ -42,20 +42,20 @@
</div>
</InputGroup>
<InputGroup @name="Backdrop" @helpText="Optional banner or background image for your organization.">
<InputGroup @name={{t "console.settings.index.backdrop"}} @helpText={{t "console.settings.index.backdrop-help-text"}}>
<div class="flex flex-row items-center">
<Image src={{@model.backdrop_url}} @fallbackSrc={{config "defaultValues.placeholderImage"}} alt={{concat @model.name " backdrop"}} class="h-20 w-64 border dark:border-gray-900 rounded-md mr-4" />
<FileUpload @name="backdrop" @accept="image/*" @onFileAdded={{fn this.uploadFile "backdrop"}} as |queue|>
<FileUpload @name={{t "console.settings.index.backdrop"}} @accept="image/*" @onFileAdded={{fn this.uploadFile "backdrop"}} as |queue|>
<a tabindex={{0}} class="flex items-center px-0 mt-2 text-xs no-underline truncate btn btn-sm btn-default">
{{#if queue.files.length}}
<Spinner class="mr-1" />
<span>
Uploading...
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="image" class="mr-1" />
<span>
Upload new backdrop
{{t "console.settings.index.upload-new-backdrop"}}
</span>
{{/if}}
</a>

View File

@@ -0,0 +1,36 @@
{{page-title "TwoFa"}}
<Layout::Section::Header @title="2FA" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="mb-3">
{{#if this.loadCompanyTwoFaSettings.isIdle}}
<TwoFaSettings
@showEnforceOption={{true}}
@showMethodSelection={{false}}
@twoFaMethods={{this.methods}}
@twoFaSettings={{this.twoFaSettings}}
@onTwoFaToggled={{this.onTwoFaToggled}}
@onTwoFaMethodSelected={{this.onTwoFaMethodSelected}}
@onTwoFaEnforcedToggled={{this.onTwoFaEnforceToggled}}
/>
{{else}}
<div class="flex items-center justify-center p-4">
<Spinner @loadingMessage="Loading User 2FA Settings..." @wrapperClass="flex flex-row" @iconClass="mr-2" />
</div>
{{/if}}
</div>
<Button
@type="primary"
@buttonType="submit"
@text="Save 2FA Settings"
@icon="save"
@onClick={{this.saveTwoFactor}}
@isLoading={{this.enforceTwoFaForCompanyUsers.isRunning}}
/>
</ContentPanel>
</div>
</div>
</Layout::Section::Body>

View File

@@ -3,7 +3,7 @@
<div class="border border-black bg-gray-900 shadow-md rounded-md p-5 mt-12">
<div class="w-full flex-col items-center justify-center text-center mb-6">
<img src="/images/icon.png" alt={{concat (t "app.name") " Install"}} class="w-12 h-12 mx-auto" width="48" height="48" />
<h3 class="mt-2 text-gray-50 font-bold">{{t "app.name"}} Installer</h3>
<h3 class="mt-2 text-gray-50 font-bold">{{t "app.name"}} {{t "install.installer-header"}}</h3>
</div>
<div class="space-y-4 mb-5">
{{#each this.steps as |step|}}
@@ -24,10 +24,10 @@
{{#if this.error}}
<div class="flex items-center border border-red-900 bg-red-800 text-red-100 px-4 py-1.5 rounded-lg mb-3 shadow-md">
<FaIcon @icon="triangle-exclamation" class="text-red-100 mr-2" />
<span>The install failed! Click the button below to retry the install.</span>
<span>{{t "install.failed-message-sent"}}</span>
</div>
{{/if}}
<Button @type="primary" @icon="play" @size="lg" @text={{if this.error "Retry Install" "Start Install"}} @wrapperClass="flex-1" class="w-full" @onClick={{this.startInstall}} @isLoading={{this.install.isRunning}} @disabled={{this.install.isRunning}} />
<Button @type="primary" @icon="play" @size="lg" @text={{if this.error (t "install.retry-install") (t "install.start-install") }} @wrapperClass="flex-1" class="w-full" @onClick={{this.startInstall}} @isLoading={{this.install.isRunning}} @disabled={{this.install.isRunning}} />
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@
<div class="mb-8">
<img class="mx-auto h-12 w-auto" src={{@brand.icon_url}} alt={{t "app.name"}}>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
You've been invited to join {{@model.name}}
{{t "invite.for-users.invitation-message" companyName=@model.name}}
</h2>
</div>
@@ -13,22 +13,22 @@
<FaIcon @icon="info-circle" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
You've been invited to join the {{@model.name}} organization on {{t "app.name"}}. To accept this invitation, input your invitation code received by email and click continue.
{{t "invite.invitation-sent-message" htnmlSafe=true companyName=@model.name appName=(t "app.name")}}
</p>
</div>
<form class="space-y-6" {{on "submit" this.acceptInvite}}>
<div>
<label for="code" class="block text-sm font-medium text-gray-700 dark:text-gray-50">
Your invitiation code
{{t "invite.for-users.invitation-code-sent-text"}}
</label>
<div class="mt-2">
<Input @value={{this.code}} id="code" name="code" @type="code" required class="form-input form-input-lg w-full" placeholder="Your invitiation code" />
<Input @value={{this.code}} id="code" name="code" @type="code" required class="form-input form-input-lg w-full" placeholder={{t "invite.for-users.invitation-code-sent-text"}} />
</div>
</div>
<div>
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text="Accept Invitation" @onClick={{this.acceptInvite}} @isLoading={{this.isLoading}} />
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text={{t "invite.for-users.accept-invitation-text"}} @onClick={{this.acceptInvite}} @isLoading={{this.isLoading}} />
</div>
</form>
</div>

View File

@@ -2,7 +2,7 @@
<div class="mb-4">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
Create your account
{{t "onboard.index.title"}}
</h2>
</div>
@@ -11,8 +11,8 @@
<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">
<strong>Welcome to {{t "app.name"}}!</strong><br />
Complete the details required below to get started.
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
@@ -20,17 +20,17 @@
{{#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="Your full name" @value={{this.name}} @helpText="Your full name." @inputClass="input-lg" />
<InputGroup @name="Your email address" @type="email" @value={{this.email}} @helpText="Your email address." @inputClass="input-lg" />
<InputGroup @name="Your phone number" @helpText="Your phone number.">
<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="Organization name" @value={{this.organization_name}} @helpText="Your organization name, all your services and resources will be managed under this organization, later you can create as many organizations as you want or need." @inputClass="input-lg" />
<InputGroup @name="Enter a password" @value={{this.password}} @type="password" @helpText="Your password, make sure it's a good one." @inputClass="input-lg" />
<InputGroup @name="Confirm your password" @value={{this.password_confirmation}} @type="password" @helpText="Just to confirm the password you entered above." @inputClass="input-lg" />
<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 @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text="Continue" @isLoading={{this.isLoading}} @disabled={{this.readyToSubmit}} @onClick={{this.startOnboard}} />
<Button @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text={{t "onboard.index.continue-button-text"}} @isLoading={{this.isLoading}} @disabled={{this.readyToSubmit}} @onClick={{this.startOnboard}} />
</div>
</form>
</div>

View File

@@ -1,8 +1,9 @@
{{page-title "Account Verification"}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-8">
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
<img class="mx-auto h-12 w-auto" src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
Verify your email address
{{t "onboard.verify-email.title"}}
</h2>
</div>
@@ -11,15 +12,15 @@
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
<strong>Almost done!</strong><br> Check your email for a verification code.
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</p>
</div>
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
<InputGroup @type="tel" @name="Verification Code" @value={{this.code}} @helpText="Enter the verification code you received via email." @inputClass="input-lg" {{on "input" this.validateInput}} />
<InputGroup @type="tel" @name="Verification Code" @value={{this.code}} @helpText={{t "onboard.verify-email.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} />
<div>
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text={{t "onboard.verify-email.verify-button-text"}} @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
</div>
{{#if this.stillWaiting}}
@@ -29,14 +30,14 @@
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
</div>
<div class="ml-3 flex items-center">
<span class="text-lg font-extrabold text-yellow-800">Didn't receive an email yet?</span>
<span class="text-lg font-extrabold text-yellow-800">{{t "onboard.verify-email.not-sent.message"}}</span>
</div>
</div>
<div class="py-3">
<p class="text-yellow-700">Use alternaitve options below to verify your account.</p>
<p class="text-yellow-700">{{t "onboard.verify-email.not-sent.alternative-choice"}}</p>
<div class="flex items-center mt-3">
<Button @type="default" class="mr-2" @onClick={{this.resendEmail}}>Resend Email</Button>
<Button @type="default" @onClick={{this.resendBySms}}>Send by SMS</Button>
<Button @type="default" class="mr-2" @onClick={{this.resendEmail}}>{{t "onboard.verify-email.not-sent.resent-button-text"}}</Button>
<Button @type="default" @onClick={{this.resendBySms}}>{{t "onboard.verify-email.not-sent.sms-button-text"}}</Button>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
export default function getTwoFaMethods() {
return [
// {
// key: 'authenticator_app',
// name: 'Authenticator App',
// description: 'Get codes from an app like Authy, 1Password, Microsoft Authenticator, or Google Authenticator',
// recommended: true,
// },
{ key: 'sms', name: 'SMS', description: 'Receive a unique code via SMS' },
{ key: 'email', name: 'Email', description: 'Receive a unique code via Email' },
];
}

View File

@@ -3,7 +3,7 @@
"packages": [
{
"name": "ember-cli",
"version": "4.6.0",
"version": "5.4.1",
"blueprints": [
{
"name": "app",

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