mirror of
https://github.com/fleetbase/fleetbase.git
synced 2025-12-19 14:18:57 +00:00
Feature: Customer Portal, improved Customer, Contact, and Vendor Management
This commit is contained in:
5
.github/workflows/cd.yml
vendored
5
.github/workflows/cd.yml
vendored
@@ -132,11 +132,14 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Check for _GITHUB_AUTH_TOKEN and create .npmrc
|
||||
- name: Create and Setup .npmrc
|
||||
run: |
|
||||
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
|
||||
echo "//npm.pkg.github.com/:_authToken=${{ secrets._GITHUB_AUTH_TOKEN }}" > .npmrc
|
||||
fi
|
||||
if [[ -n "${{ secrets.FLEETBASE_REGISTRY_TOKEN }}" ]]; then
|
||||
echo "//registry.fleetbase.io/:_authToken=${{ secrets.FLEETBASE_REGISTRY_TOKEN }}" > .npmrc
|
||||
fi
|
||||
working-directory: ./console
|
||||
|
||||
- name: Set Env Variables for QA
|
||||
|
||||
@@ -4,25 +4,8 @@ import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class AuthForgotPasswordController extends Controller {
|
||||
/**
|
||||
* Inject the `fetch` service
|
||||
*
|
||||
* @memberof AuthForgotPasswordController
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Inject the `notifications` service
|
||||
*
|
||||
* @memberof AuthForgotPasswordController
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Inject the `intl` service
|
||||
*
|
||||
* @memberof AuthForgotPasswordController
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,53 +5,12 @@ import { action } from '@ember/object';
|
||||
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
|
||||
|
||||
export default class AuthLoginController extends Controller {
|
||||
/**
|
||||
* Inject the `forgotPassword` controller
|
||||
*
|
||||
* @var {Controller}
|
||||
*/
|
||||
@controller('auth.forgot-password') forgotPasswordController;
|
||||
|
||||
/**
|
||||
* Inject the `notifications` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Inject the `urlSearchParams` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service urlSearchParams;
|
||||
|
||||
/**
|
||||
* Inject the `session` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* Inject the `router` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* Inject the `intl` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Inject the `fetch` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
@@ -110,8 +69,20 @@ export default class AuthLoginController extends Controller {
|
||||
*/
|
||||
@tracked failedAttempts = 0;
|
||||
|
||||
/**
|
||||
* Authentication token.
|
||||
*
|
||||
* @memberof AuthLoginController
|
||||
*/
|
||||
@tracked token;
|
||||
|
||||
/**
|
||||
* Action to login user.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
* @memberof AuthLoginController
|
||||
*/
|
||||
@action async login(event) {
|
||||
// firefox patch
|
||||
event.preventDefault();
|
||||
|
||||
270
console/app/controllers/auth/portal-login.js
Normal file
270
console/app/controllers/auth/portal-login.js
Normal file
@@ -0,0 +1,270 @@
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
|
||||
|
||||
export default class AuthPortalLoginController extends Controller {
|
||||
@controller('auth.forgot-password') forgotPasswordController;
|
||||
@service notifications;
|
||||
@service urlSearchParams;
|
||||
@service session;
|
||||
@service router;
|
||||
@service intl;
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Whether or not to remember the users session
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked rememberMe = false;
|
||||
|
||||
/**
|
||||
* The identity to authenticate with
|
||||
*
|
||||
* @var {String}
|
||||
*/
|
||||
@tracked identity = null;
|
||||
|
||||
/**
|
||||
* The password to authenticate with
|
||||
*
|
||||
* @var {String}
|
||||
*/
|
||||
@tracked password = null;
|
||||
|
||||
/**
|
||||
* Login is validating user input
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked isValidating = false;
|
||||
|
||||
/**
|
||||
* Login is processing
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked isLoading = false;
|
||||
|
||||
/**
|
||||
* If the connection or requesst it taking too long
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked isSlowConnection = false;
|
||||
|
||||
/**
|
||||
* Interval to determine when to timeout the request
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked timeout = null;
|
||||
|
||||
/**
|
||||
* Number of failed login attempts
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked failedAttempts = 0;
|
||||
|
||||
/**
|
||||
* Authentication token.
|
||||
*
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@tracked token;
|
||||
|
||||
/**
|
||||
* Action to login user.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@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'));
|
||||
}
|
||||
|
||||
// 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 {
|
||||
this.session.setRedirect('portal');
|
||||
await this.session.authenticate('authenticator:fleetbase', { identity, password }, rememberMe);
|
||||
} catch (error) {
|
||||
this.failedAttempts++;
|
||||
|
||||
// Handle unverified user
|
||||
if (error.toString().includes('not verified')) {
|
||||
return this.sendUserForEmailVerification(identity);
|
||||
}
|
||||
|
||||
// Handle password reset required
|
||||
if (error.toString().includes('reset required')) {
|
||||
return this.sendUserForPasswordReset(identity);
|
||||
}
|
||||
|
||||
return this.failure(error);
|
||||
}
|
||||
|
||||
if (this.session.isAuthenticated) {
|
||||
this.success();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition user to onboarding screen
|
||||
*/
|
||||
@action transitionToOnboard() {
|
||||
return this.router.transitionTo('onboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition to forgot password screen, if email is set - set it.
|
||||
*/
|
||||
@action forgotPassword() {
|
||||
return this.router.transitionTo('auth.forgot-password').then(() => {
|
||||
if (this.email) {
|
||||
this.forgotPasswordController.email = this.email;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an email verification session and transitions user to verification route.
|
||||
*
|
||||
* @param {String} email
|
||||
* @return {Promise<Transition>}
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@action sendUserForEmailVerification(email) {
|
||||
return this.fetch.post('auth/create-verification-session', { email, send: true }).then(({ token, session }) => {
|
||||
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, hello: session } }).then(() => {
|
||||
this.reset('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends user to forgot password flow.
|
||||
*
|
||||
* @param {String} email
|
||||
* @return {Promise<Transition>}
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@action sendUserForPasswordReset(email) {
|
||||
this.notifications.warning(this.intl.t('auth.login.password-reset-required'));
|
||||
return this.router.transitionTo('auth.forgot-password', { queryParams: { email } }).then(() => {
|
||||
this.reset('error');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets correct route to send user to after login.
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
setRedirect() {
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
|
||||
if (shift) {
|
||||
this.session.setRedirect(pathToRoute(shift));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the authentication success
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
success() {
|
||||
this.reset('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the authentication failure
|
||||
*
|
||||
* @param {String} error An error message
|
||||
* @void
|
||||
*/
|
||||
failure(error) {
|
||||
this.notifications.serverError(error);
|
||||
this.reset('error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the request slow connection
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
slowConnection() {
|
||||
this.notifications.error(this.intl.t('auth.login.slow-connection-message'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the login form
|
||||
*
|
||||
* @param {String} type
|
||||
* @void
|
||||
*/
|
||||
reset(type) {
|
||||
// reset login form state
|
||||
this.isLoading = false;
|
||||
this.isSlowConnection = false;
|
||||
// reset login form state depending on type of reset
|
||||
switch (type) {
|
||||
case 'success':
|
||||
this.identity = null;
|
||||
this.password = null;
|
||||
this.isValidating = false;
|
||||
break;
|
||||
case 'error':
|
||||
case 'fail':
|
||||
this.password = null;
|
||||
break;
|
||||
}
|
||||
// clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
17
console/app/controllers/portal.js
Normal file
17
console/app/controllers/portal.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class PortalController extends Controller {
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* Action to invalidate and log user out
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
@action invalidateSession(event) {
|
||||
event.preventDefault();
|
||||
this.session.invalidateWithLoader();
|
||||
}
|
||||
}
|
||||
16
console/app/routes/auth/portal-login.js
Normal file
16
console/app/routes/auth/portal-login.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class AuthPortalLoginRoute extends Route {
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* If user is authentication redirect to portal.
|
||||
*
|
||||
* @memberof AuthPortalLoginRoute
|
||||
* @void
|
||||
*/
|
||||
beforeModel() {
|
||||
this.session.prohibitAuthentication('portal');
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import '@fleetbase/leaflet-routing-machine';
|
||||
|
||||
export default class ConsoleRoute extends Route {
|
||||
@service store;
|
||||
@service session;
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* Require authentication to access all `console` routes.
|
||||
@@ -14,9 +14,13 @@ export default class ConsoleRoute extends Route {
|
||||
* @return {Promise}
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
@action async beforeModel(transition) {
|
||||
async beforeModel (transition) {
|
||||
this.session.requireAuthentication(transition, 'auth.login');
|
||||
|
||||
if (this.session.data.authenticated.type === 'customer') {
|
||||
return this.router.transitionTo('portal');
|
||||
}
|
||||
|
||||
return this.session.promiseCurrentUser(transition);
|
||||
}
|
||||
|
||||
@@ -26,7 +30,7 @@ export default class ConsoleRoute extends Route {
|
||||
* @return {BrandModel}
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
model() {
|
||||
model () {
|
||||
return this.store.findRecord('brand', 1);
|
||||
}
|
||||
}
|
||||
|
||||
30
console/app/routes/portal.js
Normal file
30
console/app/routes/portal.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class PortalRoute extends Route {
|
||||
@service store;
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* Require authentication to access all `portal` routes.
|
||||
*
|
||||
* @param {Transition} transition
|
||||
* @return {Promise}
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
async beforeModel(transition) {
|
||||
this.session.requireAuthentication(transition, 'auth.portal-login');
|
||||
|
||||
return this.session.promiseCurrentUser(transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the branding settings.
|
||||
*
|
||||
* @return {BrandModel}
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
model() {
|
||||
return this.store.findRecord('brand', 1);
|
||||
}
|
||||
}
|
||||
3
console/app/routes/portal/home.js
Normal file
3
console/app/routes/portal/home.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class PortalHomeRoute extends Route {}
|
||||
@@ -28,7 +28,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6" {{on "submit" this.sendSecureLink}}>
|
||||
<form class="space-y-6" {{on "submit" (perform 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"}}
|
||||
|
||||
@@ -8,16 +8,18 @@
|
||||
</div>
|
||||
|
||||
{{#if (gte this.failedAttempts 3)}}
|
||||
<div class="px-3 py-2 my-6 rounded-md shadow-sm bg-yellow-200">
|
||||
<div class="flex mb-5">
|
||||
<div>
|
||||
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-900 mr-4" />
|
||||
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
|
||||
<div class="flex flex-row items-start mb-2">
|
||||
<div class="w-8 flex-grow-0 flex-shrink-0">
|
||||
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="flex-1 text-sm text-yellow-100">
|
||||
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-yellow-900 dark:yellow-red-900">
|
||||
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="warning" @onClick={{this.forgotPassword}} />
|
||||
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="link" class="text-yellow-100" @wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50" @onClick={{this.forgotPassword}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -72,11 +74,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="mt-6 space-y-4">
|
||||
<Button @buttonType="submit" @type="primary" @text={{t "auth.login.form.sign-in-button"}} @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<Button @text={{t "auth.login.form.create-account-button"}} @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
|
||||
<Button @text="Customer Login" @type="link" @wrapperClass="btn-block py-1 border dark:border-gray-700 border-gray-200 hover:opacity-50" @disabled={{this.isLoading}} @onClick={{fn (transition-to "auth.portal-login")}} />
|
||||
</div>
|
||||
</form>
|
||||
88
console/app/templates/auth/portal-login.hbs
Normal file
88
console/app/templates/auth/portal-login.hbs
Normal file
@@ -0,0 +1,88 @@
|
||||
<div>
|
||||
<div class="mx-auto w-12 h-12">
|
||||
<LogoIcon @url={{@brand.icon_url}} @size="12" class="mx-auto" />
|
||||
</div>
|
||||
<h2 class="mt-6 mb-3 text-3xl font-semibold leading-9 text-center text-gray-900 dark:text-gray-100">
|
||||
Customer Login
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{{#if (gte this.failedAttempts 3)}}
|
||||
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
|
||||
<div class="flex flex-row items-start mb-2">
|
||||
<div class="w-8 flex-grow-0 flex-shrink-0">
|
||||
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="flex-1 text-sm text-yellow-100">
|
||||
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="link" class="text-yellow-100" @wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50" @onClick={{this.forgotPassword}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<form class="mt-8" {{on "submit" this.login}}>
|
||||
<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}}
|
||||
/>
|
||||
</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}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-6">
|
||||
<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"}}
|
||||
</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>
|
||||
</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}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-8">
|
||||
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
|
||||
<img class="mx-auto h-12 w-auto" src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} />
|
||||
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "auth.verification.title"}}
|
||||
</h2>
|
||||
@@ -13,34 +13,66 @@
|
||||
<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}}
|
||||
{{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}} {{did-insert this.validateInitInput}} />
|
||||
<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}}
|
||||
{{did-insert this.validateInitInput}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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="javascript:;" {{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 class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
|
||||
<div class="flex flex-row items-start mb-2">
|
||||
<div class="w-8 flex-grow-0 flex-shrink-0">
|
||||
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
|
||||
</div>
|
||||
<div class="ml-3 flex items-center">
|
||||
<span class="text-lg font-extrabold text-yellow-800">{{t "auth.verification.didnt-receive-a-code"}}</span>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 text-sm text-yellow-100">
|
||||
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
|
||||
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="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 class="flex items-center space-x-2">
|
||||
<Button
|
||||
@text={{t "auth.verification.not-sent.resend-email"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
|
||||
@onClick={{this.resendEmail}}
|
||||
/>
|
||||
<Button
|
||||
@text={{t "auth.verification.not-sent.send-by-sms"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
|
||||
@onClick={{this.resendBySms}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -26,21 +26,35 @@
|
||||
</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 class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
|
||||
<div class="flex flex-row items-start mb-2">
|
||||
<div class="w-8 flex-grow-0 flex-shrink-0">
|
||||
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
|
||||
</div>
|
||||
<div class="ml-3 flex items-center">
|
||||
<span class="text-lg font-extrabold text-yellow-800">{{t "onboard.verify-email.didnt-receive-a-code"}}</span>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 text-sm text-yellow-100">
|
||||
<div>{{t "onboard.verify-email.didnt-receive-a-code" htmlSafe=true}}</div>
|
||||
<div>{{t "onboard.verify-email.not-sent.alternative-choice" htmlSafe=true}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-1">
|
||||
<p class="text-yellow-700 text-sm">{{t "onboard.verify-email.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 "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>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Button
|
||||
@text={{t "onboard.verify-email.not-sent.resend-email"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
|
||||
@onClick={{this.resendEmail}}
|
||||
/>
|
||||
<Button
|
||||
@text={{t "onboard.verify-email.not-sent.send-by-sms"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
|
||||
@onClick={{this.resendBySms}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
8
console/app/templates/portal.hbs
Normal file
8
console/app/templates/portal.hbs
Normal file
@@ -0,0 +1,8 @@
|
||||
{{page-title "Portal"}}
|
||||
<div class="portal-container">
|
||||
<div class="container">
|
||||
<div class="flex flex-row">
|
||||
<a href="javascript:;" {{on "click" this.invalidateSession}}>Sign out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
2
console/app/templates/portal/home.hbs
Normal file
2
console/app/templates/portal/home.hbs
Normal file
@@ -0,0 +1,2 @@
|
||||
{{page-title "Home"}}
|
||||
{{outlet}}
|
||||
@@ -13,6 +13,7 @@ Router.map(function () {
|
||||
this.route('reset-password', { path: '/reset-password/:id' });
|
||||
this.route('two-fa');
|
||||
this.route('verification');
|
||||
this.route('portal-login', { path: '/portal' });
|
||||
});
|
||||
this.route('onboard', function () {
|
||||
this.route('verify-email');
|
||||
@@ -58,5 +59,6 @@ Router.map(function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('portal');
|
||||
this.route('install');
|
||||
});
|
||||
|
||||
12
console/tests/unit/controllers/auth/portal-login-test.js
Normal file
12
console/tests/unit/controllers/auth/portal-login-test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | auth/portal-login', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:auth/portal-login');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
12
console/tests/unit/controllers/portal-test.js
Normal file
12
console/tests/unit/controllers/portal-test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | portal', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:portal');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
11
console/tests/unit/routes/auth/portal-login-test.js
Normal file
11
console/tests/unit/routes/auth/portal-login-test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | auth/portal-login', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:auth/portal-login');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
11
console/tests/unit/routes/portal-test.js
Normal file
11
console/tests/unit/routes/portal-test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | portal', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:portal');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
11
console/tests/unit/routes/portal/home-test.js
Normal file
11
console/tests/unit/routes/portal/home-test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | portal/home', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:portal/home');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
Submodule packages/core-api updated: af272fac24...5de169c920
Submodule packages/fleetops updated: fac2832a54...21068c1f42
Submodule packages/fleetops-data updated: e385e8edcf...a4f1eb7b41
Reference in New Issue
Block a user