Compare commits

..

4 Commits

Author SHA1 Message Date
Ronald A. Richardson
dd65ee619b upgraded to v0.3.4 2023-12-27 12:01:39 +08:00
Ronald A. Richardson
e790a0e123 upgraded to v0.3.3 2023-12-27 11:07:33 +08:00
Ronald A. Richardson
723e3ca3d1 fix composer file 2023-11-27 10:23:44 +08:00
Ronald A. Richardson
4eb706d33e upgraded to v0.3.1 / cloud version 2023-11-27 10:19:56 +08:00
127 changed files with 1791 additions and 10207 deletions

1
.gitignore vendored
View File

@@ -27,7 +27,6 @@ verdaccio/storage
packages/billing
packages/flespi
packages/loconav
packages/internals
packages/projectargus-engine
# wip
packages/solid

View File

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

View File

@@ -9,9 +9,10 @@
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"fleetbase/core-api": "^1.3.13",
"fleetbase/fleetops-api": "^0.4.4",
"fleetbase/storefront-api": "^0.2.9",
"fleetbase/core-api": "^1.3.5",
"fleetbase/fleetops-api": "^0.3.7",
"fleetbase/storefront-api": "^0.2.5",
"fleetbase/billing-api": "^0.0.5",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.75",
@@ -32,6 +33,12 @@
"nunomaduro/collision": "^5.10",
"phpunit/phpunit": "^9.5.10"
},
"repositories": [
{
"type": "vcs",
"url": "git@github.com:fleetbase/billing.git"
}
],
"autoload": {
"psr-4": {
"App\\": "app/",

1298
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ return [
|
*/
'default' => env('FILESYSTEM_DRIVER', 'public'),
'default' => env('FILESYSTEM_DRIVER', 'local'),
/*
|--------------------------------------------------------------------------

View File

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

5622
api/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@ module.exports = {
'ember/no-empty-glimmer-component-classes': 'off',
'ember/no-get': 'off',
'ember/classic-decorator-no-classic-methods': 'off',
'n/no-unpublished-require': [
'node/no-unpublished-require': [
'error',
{
allowModules: [

1
console/.gitignore vendored
View File

@@ -4,7 +4,6 @@
# dependencies
/node_modules/
/scripts/node_modules/
# misc
/.env*

View File

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

View File

@@ -6,6 +6,7 @@ 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,5 +1,5 @@
# ---- Build Stage ----
FROM node:18.15.0-alpine AS builder
FROM node:16.20-alpine AS builder
# Set the working directory in the container to /app
WORKDIR /app

View File

@@ -14,7 +14,7 @@ export default class App extends Application {
async ready() {
const extensions = await loadExtensions();
this.extensions = extensions;
this.engines = mapEngines(extensions);
}

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
<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

@@ -1,144 +0,0 @@
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

@@ -1,14 +0,0 @@
{{#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

@@ -1,74 +0,0 @@
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

@@ -1,31 +0,0 @@
<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

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

View File

@@ -33,6 +33,7 @@ export default class AuthLoginController extends Controller {
*/
@service session;
/**
* Inject the `router` service
*
@@ -40,20 +41,6 @@ export default class AuthLoginController extends Controller {
*/
@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
*
@@ -110,62 +97,29 @@ export default class AuthLoginController extends Controller {
*/
@tracked failedAttempts = 0;
@tracked token;
/**
* Authenticate the user
*
* @void
*/
@action async login(event) {
// firefox patch
event.preventDefault();
// get user credentials
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'));
}
const { email, password, rememberMe } = this;
// start loader
this.set('isLoading', true);
// set where to redirect on login
this.setRedirect();
// send request to check for 2fa
try {
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);
await this.session.authenticate('authenticator:fleetbase', { email, password }, rememberMe);
} catch (error) {
this.failedAttempts++;
// Handle unverified user
if (error.toString().includes('not verified')) {
return this.sendUserForEmailVerification(identity);
}
return this.failure(error);
}
@@ -192,30 +146,6 @@ export default class AuthLoginController extends Controller {
});
}
/**
* 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.
*
@@ -255,7 +185,7 @@ export default class AuthLoginController extends Controller {
* @void
*/
slowConnection() {
this.notifications.error(this.intl.t('auth.login.slow-connection-message'));
this.notifications.error('Experiencing connectivity issues.');
}
/**

View File

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

View File

@@ -1,271 +0,0 @@
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

@@ -1,239 +0,0 @@
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
);
}
/**
* Allow user to manually trigger no code received prompt.
*
* @memberof AuthVerificationController
*/
@action onDidntReceiveCode() {
this.stillWaiting = true;
}
/**
* 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

@@ -115,21 +115,22 @@ export default class ConsoleController extends Controller {
*/
constructor() {
super(...arguments);
this.router.on('routeDidChange', (transition) => {
if (this.sidebarContext) {
// 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();
@@ -137,7 +138,7 @@ export default class ConsoleController extends Controller {
// Otherwise, show the sidebar
this.sidebarContext.show();
}
// Ensure toggle is enabled unless on a hidden route
this.sidebarToggleEnabled = !shouldHideSidebar;
}
@@ -154,7 +155,7 @@ export default class ConsoleController extends Controller {
@action onSidebarToggle(sidebar, isOpen) {
this.sidebarToggleState = {
clicked: true,
isOpen,
isOpen
};
}

View File

@@ -1,248 +0,0 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { 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

@@ -1,172 +0,0 @@
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

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

View File

@@ -1,184 +0,0 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { 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

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

View File

@@ -1,7 +1,133 @@
import AuthVerificationController from '../auth/verification';
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 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;
/**
* Inject the `router` service
*
* @memberof OnboardIndexController
*/
@service router;
/**
* 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.
*
@@ -9,24 +135,19 @@ export default class OnboardVerifyEmailController extends AuthVerificationContro
* @memberof OnboardVerifyEmailController
*/
@action verifyCode() {
const { hello, code } = this;
const session = this.hello;
const code = this.code;
this.isLoading = true;
return this.fetch
.post('onboard/verify-email', { session: hello, code })
.then(({ status, token }) => {
if (status === 'ok') {
.post('onboard/verify-email', { session, code })
.then((response) => {
if (response.status === 'success') {
this.notifications.success('Email successfully verified!');
this.notifications.info('Welcome to Fleetbase!');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.session.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
return this.router.transitionTo('console');
}
})
.catch((error) => {
@@ -36,4 +157,46 @@ export default class OnboardVerifyEmailController extends AuthVerificationContro
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

@@ -1,21 +0,0 @@
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

@@ -1,44 +0,0 @@
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
import { computed } from '@ember/object';
import { format, formatDistanceToNow } from 'date-fns';
export default class CommentModel extends Model {
/** @ids */
@attr('string') company_uuid;
@attr('string') parent_comment_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
/** @relationships */
@belongsTo('user') author;
@belongsTo('comment', { inverse: 'replies' }) parent;
@hasMany('comment', { inverse: 'parent' }) replies;
/** @attributes */
@attr('string') content;
@attr('boolean') editable;
@attr('raw') tags;
@attr('raw') meta;
/** @dates */
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
/** @computed */
@computed('created_at') get createdAgo() {
return formatDistanceToNow(this.created_at);
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'PPP p');
}
@computed('updated_at') get updatedAgo() {
return formatDistanceToNow(this.updated_at);
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'PPP p');
}
}

View File

@@ -79,11 +79,6 @@ 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

@@ -10,9 +10,7 @@ Router.map(function () {
this.route('auth', function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
this.route('reset-password');
});
this.route('onboard', function () {
this.route('verify-email');
@@ -27,11 +25,9 @@ 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 () {
@@ -47,9 +43,33 @@ Router.map(function () {
});
this.route('branding');
this.route('notifications');
this.route('two-fa-settings');
this.route('virtual', { path: '/:slug/:view' });
});
this.mount('@fleetbase/billing-engine', {
as: 'billing',
path: 'billing'
});
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

@@ -4,14 +4,7 @@ import { inject as service } from '@ember/service';
export default class AuthResetPasswordRoute extends Route {
@service store;
async model(params) {
return params;
}
async setupController(controller) {
super.setupController(...arguments);
// set brand to controller
this.brand = await this.store.findRecord('brand', 1);
model() {
return this.store.findRecord('brand', 1);
}
}

View File

@@ -1,114 +0,0 @@
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

@@ -1,33 +0,0 @@
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,13 +25,6 @@ export default class ConsoleRoute extends Route {
*/
@service session;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `currentUser` service
*
@@ -79,12 +72,6 @@ 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

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

View File

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

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
export default class CommentSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
/**
* Embedded relationship attributes
*
* @var {Object}
*/
get attrs() {
return {
author: { embedded: 'always' },
parent: { embedded: 'always' },
replies: { embedded: 'always' },
};
}
serializeAttribute(snapshot, json, key, attributes) {
if (key === 'editable') {
return;
}
super.serializeAttribute(...arguments);
}
serializeHasMany(snapshot, json, relationship) {
let key = relationship.key;
if (key === 'replies') {
return;
} else {
super.serializeHasMany(...arguments);
}
}
serializeBelongsTo() {
return;
}
}

View File

@@ -1,4 +0,0 @@
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

@@ -2,4 +2,3 @@
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'inter-ui/inter.css';
@import 'console.css';

View File

@@ -1,25 +0,0 @@
.two-fa-enforcement-alert svg.fa-triangle-exclamation {
font-size: 2.25rem;
padding-right: 0.5rem;
color: rgb(202 138 4);
}
body[data-theme='dark'] .btn.btn-warning-alert.btn-warning,
.btn.btn-warning-alert.btn-warning,
.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 (t "auth.forgot-password.is-sent.title") (t "auth.forgot-password.not-sent.title")}}
{{if this.isSent "Almost done!" "Forgot your password?"}}
</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">
{{t "auth.forgot-password.is-sent.message" htmlSafe=true}}
<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.
</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">
{{t "auth.forgot-password.not-sent.message" htmlSafe=true appName=(t "app.name")}}
<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.
</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">
{{t "auth.forgot-password.form.email-label"}}
Your email address
</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={{t "auth.forgot-password.form.email-label"}} />
<Input @value={{this.email}} @type="email" id="email" name="email" required class="form-input form-input-lg w-full" placeholder="Your email" />
</div>
</div>
<div class="flex flex-row space-x-2">
<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}} />
<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}} />
</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">
{{t "auth.login.title"}}
Sign in to your account
</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">
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
<strong>Forgot your password?</strong><br> Click the button below to reset your password.
</p>
</div>
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="warning" @onClick={{this.forgotPassword}} />
<Button @text="Ok, help me reset!" @type="warning" @onClick={{this.forgotPassword}} />
</div>
{{/if}}
@@ -25,30 +25,10 @@
<input type="hidden" name="remember" value="true" />
<div class="rounded-md shadow-sm">
<div>
<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}}
/>
<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}} />
</div>
<div class="-mt-px">
<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}}
/>
<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}} />
</div>
</div>
@@ -56,27 +36,22 @@
<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">
{{t "auth.login.form.remember-me-label"}}
Remember me
</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"
>
{{t "auth.login.form.forgot-password-label"}}
<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>
</div>
</div>
<div class="mt-6">
<Button @buttonType="submit" @type="primary" @text={{t "auth.login.form.sign-in-button"}} @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
<Button @buttonType="submit" @type="primary" @text="Sign In" @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
</div>
<div class="mt-3">
<Button @text={{t "auth.login.form.create-account-button"}} @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
<Button @text="Create a new Account" @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
</div>
</form>

View File

@@ -1,19 +1,19 @@
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg">
<div class="mb-4">
<Image src={{this.brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<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">
{{t "auth.reset-password.title"}}
Reset your password
</h2>
</div>
<form class="space-y-6" {{on "submit" this.resetPassword}}>
<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"}} />
<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." />
<div class="flex space-x-2">
<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}} />
<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}} />
</div>
</form>
</div>

View File

@@ -1,47 +0,0 @@
<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

@@ -1,48 +0,0 @@
{{page-title (t "auth.verification.header-title")}}
<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">
{{t "auth.verification.title"}}
</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">
{{t "auth.verification.message-text" htmlSafe=true}}
</p>
</div>
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
<InputGroup @type="tel" @name={{t "auth.verification.verification-input-label"}} @value={{this.code}} @helpText={{t "auth.verification.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} />
<div class="flex flex-row items-center space-x-4">
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
</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">{{t "auth.verification.didnt-receive-a-code"}}</span>
</div>
</div>
<div class="py-1">
<p class="text-yellow-700 text-sm">{{t "auth.verification.not-sent.alternative-choice"}}</p>
<div class="flex items-center mt-3">
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "auth.verification.not-sent.resend-email"}}</Button>
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "auth.verification.not-sent.send-by-sms"}}</Button>
</div>
</div>
</div>
{{/if}}
</form>
</div>

View File

@@ -1,11 +1,10 @@
{{page-title (t "app.name")}}
{{page-title "Console"}}
<Layout::Container>
<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>
@@ -13,9 +12,4 @@
</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>
{{!-- Add Locale Selector to Header --}}
<EmberWormhole @to="view-header-actions">
<LocaleSelector class="mr-0.5" />
</EmberWormhole>
</Layout::Container>

View File

@@ -1,8 +1,7 @@
{{page-title "Account"}}
<EmberWormhole @to="sidebar-menu-items">
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.account"}}>
<Layout::Sidebar::Panel @open={{true}} @title="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

@@ -1,57 +0,0 @@
{{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={{t "common.profile"}} />
<Layout::Section::Header @title="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={{t "common.your-profile"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="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={{t "console.account.index.photos"}} @accept="image/*" @onFileAdded={{this.uploadNewPhoto}} @labelClass="flex flex-row items-center justify-center" as |queue|>
<FileUpload @name="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>
{{t "common.uploading"}}
Uploading...
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
{{t "console.account.index.upload-new"}}
<span>
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={{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"}}>
<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.">
<PhoneInput @value={{this.user.phone}} @onInput={{fn (mut this.user.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
<InputGroup @name="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={{t "common.save-button-text"}} @onClick={{this.saveProfile}} @isLoading={{this.isLoading}} />
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="Save Changes" @onClick={{this.saveProfile}} @isLoading={{this.isLoading}} />
</div>
</div>
</form>

View File

@@ -1,35 +1,26 @@
{{page-title "Admin"}}
<EmberWormhole @to="sidebar-menu-items">
<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>
<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>
{{#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>
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
{{#each this.universe.adminMenuPanels 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.admin.virtual" menuItem}}
@item={{menuItem}}
@icon={{menuItem.icon}}
>{{menuItem.title}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
</Layout::Sidebar::Panel>
{{/each}}
<Layout::Sidebar::Panel @open={{true}} @title="System Config">
<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::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::Panel>
</EmberWormhole>

View File

@@ -1,62 +1,62 @@
{{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}} />
{{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}} />
</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-6">
<ContentPanel @title={{t "console.admin.branding.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<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">
<form class="flex flex-col" {{on "submit" this.save}}>
<div class="input-group">
<label>{{t "console.admin.branding.icon-text"}}</label>
<label>Icon</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={{t "console.admin.branding.icon-text"}} @accept="image/*" @onFileAdded={{this.uploadIcon}} @labelClass="flex flex-row items-center justify-center mb-0i" as |queue|>
<FileUpload @name="icon" @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>
{{t "common.uploading"}}
Uploading...
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
{{t "console.admin.branding.upload-new"}}
Upload new
</span>
{{/if}}
</a>
</FileUpload>
</div>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetIcon}}>{{t "console.admin.branding.reset-default"}}</a>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetIcon}}>Reset to default</a>
</div>
<div class="input-group">
<label>{{t "console.admin.branding.logo-text"}}</label>
<label>Logo</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={{t "console.admin.branding.logo-text"}} @accept="image/*" @onFileAdded={{this.uploadLogo}} @labelClass="flex flex-row items-center justify-center mb-0i" as |queue|>
<FileUpload @name="logo" @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>
{{t "common.uploading"}}
Uploading...
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
{{t "console.admin.branding.upload-new"}}
Upload new
</span>
{{/if}}
</a>
</FileUpload>
</div>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetLogo}}>{{t "console.admin.branding.reset-default"}}</a>
<a href="javascript:;" class="text-danger text-xs mt-1" {{on "click" this.unsetLogo}}>Reset to default</a>
</div>
<InputGroup @name={{t "console.admin.branding.theme"}}>
<InputGroup @name="Default 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={{t "console.admin.config.database.title"}} />
<Layout::Section::Header @title="Database Configuration" />
<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={{t "console.admin.config.filesystem.title"}} />
<Layout::Section::Header @title="Filesystem Configuration" />
<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={{t "console.admin.config.mail.title"}} />
<Layout::Section::Header @title="Mail Configuration" />
<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={{t "console.admin.config.notification-channels.title"}} />
<Layout::Section::Header @title="Notification Channels Configuration" />
<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={{t "console.admin.config.queue.title"}} />
<Layout::Section::Header @title="Queue Configuration" />
<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={{t "console.admin.config.services.title"}} />
<Layout::Section::Header @title="Services Configuration" />
<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={{t "console.admin.config.socket.title"}} />
<Layout::Section::Header @title="Socket Configuration" />
<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={{t "common.overview"}} />
<Layout::Section::Header @title="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={{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}} />
<StatWidget @title="Total Users" @value={{@model.total_users}} />
<StatWidget @title="Total Organizations" @value={{@model.total_organizations}} />
<StatWidget @title="Total Transactions" @value={{@model.total_transactions}} />
</div>
</div>
</div>

View File

@@ -1,13 +1,13 @@
{{page-title "Notifications"}}
<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 @title="Notifications">
<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">
{{#each-in this.groupedNotifications as |groupName notifications|}}
<ContentPanel @title={{concat (smart-humanize groupName) (t "console.admin.notifications.notification-settings") }} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title={{concat (smart-humanize groupName) " 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

@@ -1,28 +0,0 @@
{{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">{{t "console.extensions.title"}}</h1>
<h1 class="dark:text-gray-100 text-black text-4xl font-bold mb-4">Extensions are coming soon!</h1>
<p class="dark:text-gray-300 text-black text-lg">
{{t "console.extensions.message"}}
Please check back in the upcoming versions as we prepare to launch the Extensions repository and marketplace.
</p>
</div>
</div>

View File

@@ -1,7 +1,6 @@
{{page-title "Dashboard"}}
<Layout::Section::Body class="overflow-y-scroll h-full pt-6">
<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={{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 @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>
<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">{{t "console.notifications.received"}} {{notification.createdAgo}}</div>
<div class="text-gray-300 text-xs antialiased mt-1">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">{{t "console.notifications.message"}}</p>
<p class="text-base text-gray-800 dark:text-gray-300 italic">No notifications to display.</p>
</div>
{{/each}}
</div>

View File

@@ -1,28 +1,18 @@
{{page-title (t "common.settings")}}
{{page-title "Settings"}}
<EmberWormhole @to="sidebar-menu-items">
<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>
<Layout::Sidebar::Panel @open={{true}} @title="Settings">
<Layout::Sidebar::Item @route="console.settings.index" @icon="cog">Organization</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={{t "console.settings.index.title"}} />
<Layout::Section::Header @title="Organization Settings" />
<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={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Organization Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form {{on "submit" this.saveSettings}}>
<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"}}>
<InputGroup @name="Organization name" @value={{@model.name}} />
<InputGroup @name="Organization description" @value={{@model.description}} />
<InputGroup @name="Organization phone number">
<PhoneInput @value={{@model.phone}} @onInput={{fn (mut @model.phone)}} class="form-input w-full" />
</InputGroup>
<InputGroup @name={{t "console.settings.index.organization-currency"}}>
<InputGroup @name="Organization currency">
<CurrencySelect @value={{@model.currency}} @onSelect={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
</InputGroup>
<InputGroup @name={{t "console.settings.index.organization-id"}} @value={{@model.public_id}} @disabled={{true}} />
<InputGroup @name="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="{{t "common.save-button-text"}}" @isLoading={{this.isLoading}} />
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="Save Changes" @isLoading={{this.isLoading}} />
</div>
</form>
</ContentPanel>
<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"}}>
<ContentPanel @title="Organization Branding" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Logo" @helpText="Logo for your organization.">
<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={{t "console.settings.index.logo"}} @accept="image/*" @onFileAdded={{fn this.uploadFile "logo"}} as |queue|>
<FileUpload @name="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>
{{t "common.uploading"}}
Uploading...
</span>
{{else}}
<FaIcon @icon="image" class="mr-1" />
<span>
{{t "console.settings.index.upload-new-logo"}}
Upload new logo
</span>
{{/if}}
</a>
@@ -42,20 +42,20 @@
</div>
</InputGroup>
<InputGroup @name={{t "console.settings.index.backdrop"}} @helpText={{t "console.settings.index.backdrop-help-text"}}>
<InputGroup @name="Backdrop" @helpText="Optional banner or background image for your organization.">
<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={{t "console.settings.index.backdrop"}} @accept="image/*" @onFileAdded={{fn this.uploadFile "backdrop"}} as |queue|>
<FileUpload @name="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>
{{t "common.uploading"}}
Uploading...
</span>
{{else}}
<FaIcon @icon="image" class="mr-1" />
<span>
{{t "console.settings.index.upload-new-backdrop"}}
Upload new backdrop
</span>
{{/if}}
</a>

View File

@@ -1,36 +0,0 @@
{{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"}} {{t "install.installer-header"}}</h3>
<h3 class="mt-2 text-gray-50 font-bold">{{t "app.name"}} Installer</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>{{t "install.failed-message-sent"}}</span>
<span>The install failed! Click the button below to retry the install.</span>
</div>
{{/if}}
<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}} />
<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}} />
</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">
{{t "invite.for-users.invitation-message" companyName=@model.name}}
You've been invited to join {{@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">
{{t "invite.invitation-sent-message" htnmlSafe=true companyName=@model.name appName=(t "app.name")}}
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.
</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">
{{t "invite.for-users.invitation-code-sent-text"}}
Your invitiation code
</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={{t "invite.for-users.invitation-code-sent-text"}} />
<Input @value={{this.code}} id="code" name="code" @type="code" required class="form-input form-input-lg w-full" placeholder="Your invitiation code" />
</div>
</div>
<div>
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text={{t "invite.for-users.accept-invitation-text"}} @onClick={{this.acceptInvite}} @isLoading={{this.isLoading}} />
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text="Accept Invitation" @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">
{{t "onboard.index.title"}}
Create your account
</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">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
<strong>Welcome to {{t "app.name"}}!</strong><br />
Complete the details required below to get started.
</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={{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"}}>
<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.">
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.confirm-password"}} @value={{this.password_confirmation}} @type="password" @helpText={{t "onboard.index.confirm-password-help-text"}} @inputClass="input-lg" />
<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" />
<div class="flex items-center justify-end mt-5">
<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}} />
<Button @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text="Continue" @isLoading={{this.isLoading}} @disabled={{this.readyToSubmit}} @onClick={{this.startOnboard}} />
</div>
</form>
</div>

View File

@@ -1,10 +1,8 @@
{{page-title (t "onboard.verify-email.header-title")}}
<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">
{{t "onboard.verify-email.title"}}
Verify your email address
</h2>
</div>
@@ -13,16 +11,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">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
<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={{t "onboard.verify-email.verification-input-label"}} @value={{this.code}} @helpText={{t "onboard.verify-email.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} />
<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 class="flex flex-row items-center space-x-4">
<div>
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
</div>
{{#if this.stillWaiting}}
@@ -32,14 +29,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">{{t "onboard.verify-email.didnt-receive-a-code"}}</span>
<span class="text-lg font-extrabold text-yellow-800">Didn't receive an email yet?</span>
</div>
</div>
<div class="py-1">
<p class="text-yellow-700 text-sm">{{t "onboard.verify-email.not-sent.alternative-choice"}}</p>
<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 @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.resend-email"}}</Button>
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.send-by-sms"}}</Button>
<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>

View File

@@ -1,12 +0,0 @@
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

@@ -14,7 +14,7 @@ module.exports = function (/* environment */) {
* @type {String?}
* @default "null"
*/
fallbackLocale: 'en-us',
fallbackLocale: null,
/**
* Path where translations are stored. This is relative to the project root.

View File

@@ -2,12 +2,10 @@
const toBoolean = require('./utils/to-boolean');
const getenv = require('./utils/getenv');
const fixApiHost = require('./utils/fix-api-host');
const { version } = require('../package');
module.exports = function (environment) {
const ENV = {
modulePrefix: '@fleetbase/console',
version,
environment,
rootURL: '/',
locationType: 'history',

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.3.10",
"version": "0.3.3",
"private": true,
"description": "Fleetbase Console",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -21,19 +21,19 @@
"lint:hbs:fix": "ember-template-lint . --fix",
"lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix",
"lint:intl": "fleetbase-intl-lint",
"start": "pnpm run prebuild && ember serve",
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
"test:ember": "ember test"
},
"dependencies": {
"@fleetbase/ember-core": "^0.2.1",
"@fleetbase/ember-ui": "^0.2.10",
"@fleetbase/storefront-engine": "^0.2.9",
"@fleetbase/fleetops-engine": "^0.4.4",
"@fleetbase/fleetops-data": "^0.1.8",
"@fleetbase/dev-engine": "^0.2.1",
"@fleetbase/iam-engine": "^0.0.9",
"@fleetbase/ember-core": "^0.1.9",
"@fleetbase/ember-ui": "^0.2.8",
"@fleetbase/storefront-engine": "^0.2.5",
"@fleetbase/fleetops-engine": "^0.3.7",
"@fleetbase/fleetops-data": "^0.1.6",
"@fleetbase/dev-engine": "^0.2.0",
"@fleetbase/iam-engine": "^0.0.8",
"@fleetbase/billing-engine": "^0.0.5",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@ember/legacy-built-in-components": "^0.4.1",
"@fortawesome/ember-fontawesome": "^0.4.1",
@@ -54,7 +54,6 @@
"postcss-nth-list": "^1.0.2"
},
"devDependencies": {
"@fleetbase/intl-lint": "^0.0.1",
"@babel/core": "^7.23.2",
"@babel/eslint-parser": "^7.22.15",
"@babel/plugin-proposal-decorators": "^7.23.2",
@@ -137,9 +136,9 @@
},
"pnpm": {
"overrides": {
"@fleetbase/fleetops-data": "^0.1.8",
"@fleetbase/ember-core": "^0.2.1",
"@fleetbase/ember-ui": "^0.2.10"
"@fleetbase/fleetops-data": "^0.1.6",
"@fleetbase/ember-core": "^0.1.9",
"@fleetbase/ember-ui": "^0.2.8"
}
},
"prettier": {

1360
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,7 @@ Router.map(function () {
this.route('auth', function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
this.route('reset-password');
});
this.route('onboard', function () {
this.route('verify-email');
@@ -27,11 +25,9 @@ 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 () {
@@ -47,7 +43,6 @@ Router.map(function () {
});
this.route('branding');
this.route('notifications');
this.route('two-fa-settings');
this.route('virtual', { path: '/:slug/:view' });
});
});

View File

@@ -1,26 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | configure/2fa', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<Configure::2fa />`);
assert.dom(this.element).hasText('');
// Template block usage:
await render(hbs`
<Configure::2fa>
template block text
</Configure::2fa>
`);
assert.dom(this.element).hasText('template block text');
});
});

View File

@@ -1,26 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | locale-selector', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<LocaleSelector />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<LocaleSelector>
template block text
</LocaleSelector>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -1,26 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | two-fa-enforcement-alert', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<TwoFaEnforcementAlert />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<TwoFaEnforcementAlert>
template block text
</TwoFaEnforcementAlert>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -1,26 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | two-fa-settings', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<TwoFaSettings />`);
assert.dom(this.element).hasText('');
// Template block usage:
await render(hbs`
<TwoFaSettings>
template block text
</TwoFaSettings>
`);
assert.dom(this.element).hasText('template block text');
});
});

View File

@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | auth/two-fa', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:auth/two-fa');
assert.ok(controller);
});
});

View File

@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | auth/verification', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:auth/verification');
assert.ok(controller);
});
});

View File

@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/account/auth', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/account/auth');
assert.ok(controller);
});
});

View File

@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/admin/two-fa-settings', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/admin/two-fa-settings');
assert.ok(controller);
});
});

View File

@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/settings/auth', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/settings/auth');
assert.ok(controller);
});
});

View File

@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/settings/two-fa', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/settings/two-fa');
assert.ok(controller);
});
});

View File

@@ -1,39 +0,0 @@
import Application from '@ember/application';
import config from '@fleetbase/console/config/environment';
import { initialize } from '@fleetbase/console/instance-initializers/register-app-version';
import { module, test } from 'qunit';
import Resolver from 'ember-resolver';
import { run } from '@ember/runloop';
module('Unit | Instance Initializer | register-app-version', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
this.TestApplication.instanceInitializer({
name: 'initializer under test',
initialize,
});
this.application = this.TestApplication.create({
autoboot: false,
});
this.instance = this.application.buildInstance();
});
hooks.afterEach(function () {
run(this.instance, 'destroy');
run(this.application, 'destroy');
});
// TODO: Replace this with your real tests.
test('it works', async function (assert) {
await this.instance.boot();
assert.ok(true);
});
});

View File

@@ -1,14 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Model | comment', function (hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function (assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('comment', {});
assert.ok(model);
});
});

View File

@@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | auth/two-fa', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:auth/two-fa');
assert.ok(route);
});
});

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