Feature: Customer Portal, improved Customer, Contact, and Vendor Management

This commit is contained in:
Ronald A. Richardson
2024-09-04 12:31:10 +08:00
parent dc00ac3892
commit c60c460257
25 changed files with 607 additions and 107 deletions

View File

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

View File

@@ -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;
/**

View File

@@ -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();

View 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);
}
}

View 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();
}
}

View 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');
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

@@ -0,0 +1,2 @@
{{page-title "Home"}}
{{outlet}}

View File

@@ -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');
});

View 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);
});
});

View 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);
});
});

View 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);
});
});

View 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);
});
});

View 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);
});
});