From 5d1b2e19396ee59f7a89ae9a4684e6d7af5ac942 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 6 Nov 2025 20:33:23 +0800 Subject: [PATCH] - Made the `LogApiRequests` middleware more robust - Fixed controller validation handling - Added microsoft365/graph mail driver - Improved password requirements (including breached password check) - Patched creating duplicate users by email in IAM - Patch env mapper - Vehicle/driver tracking API doesnt fire resource lifecycle events or log requests - only tracking events - Patched `` component - Security patch on Storefront customers API - Styling updates on Storefront --- RELEASE.md | 20 ++- api/config/mail.php | 12 ++ console/app/components/configure/mail.hbs | 8 + console/app/components/configure/mail.js | 19 +++ console/app/components/onboarding/form.hbs | 42 +++++ console/app/components/onboarding/form.js | 77 +++++++++ .../components/onboarding/verify-email.hbs | 78 +++++++++ .../app/components/onboarding/verify-email.js | 53 +++++++ console/app/components/onboarding/yield.hbs | 11 ++ console/app/components/onboarding/yield.js | 27 ++++ console/app/controllers/onboard/index.js | 149 +----------------- .../register-default-onboarding-flow.js | 19 +++ console/app/routes/onboard/index.js | 11 ++ console/app/services/onboarding-context.js | 39 +++++ .../app/services/onboarding-orchestrator.js | 71 +++++++++ console/app/services/onboarding-registry.js | 31 ++++ console/app/services/user-verification.js | 114 ++++++++++++++ console/app/templates/onboard/index.hbs | 43 +---- console/router.map.js | 4 +- .../components/onboarding/form-test.js | 26 +++ .../onboarding/verify-email-test.js | 26 +++ .../components/onboarding/yield-test.js | 26 +++ .../register-default-onboarding-flow-test.js | 39 +++++ .../unit/services/onboarding-context-test.js | 12 ++ .../services/onboarding-orchestrator-test.js | 12 ++ .../unit/services/onboarding-registry-test.js | 12 ++ .../unit/services/user-verification-test.js | 12 ++ docker/Dockerfile | 2 +- packages/core-api | 2 +- packages/ember-ui | 2 +- packages/fleetops | 2 +- packages/storefront | 2 +- 32 files changed, 799 insertions(+), 204 deletions(-) create mode 100644 console/app/components/onboarding/form.hbs create mode 100644 console/app/components/onboarding/form.js create mode 100644 console/app/components/onboarding/verify-email.hbs create mode 100644 console/app/components/onboarding/verify-email.js create mode 100644 console/app/components/onboarding/yield.hbs create mode 100644 console/app/components/onboarding/yield.js create mode 100644 console/app/instance-initializers/register-default-onboarding-flow.js create mode 100644 console/app/services/onboarding-context.js create mode 100644 console/app/services/onboarding-orchestrator.js create mode 100644 console/app/services/onboarding-registry.js create mode 100644 console/app/services/user-verification.js create mode 100644 console/tests/integration/components/onboarding/form-test.js create mode 100644 console/tests/integration/components/onboarding/verify-email-test.js create mode 100644 console/tests/integration/components/onboarding/yield-test.js create mode 100644 console/tests/unit/instance-initializers/register-default-onboarding-flow-test.js create mode 100644 console/tests/unit/services/onboarding-context-test.js create mode 100644 console/tests/unit/services/onboarding-orchestrator-test.js create mode 100644 console/tests/unit/services/onboarding-registry-test.js create mode 100644 console/tests/unit/services/user-verification-test.js diff --git a/RELEASE.md b/RELEASE.md index 07aa61b1..717b76ac 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,16 +1,20 @@ -# 🚀 Fleetbase v0.7.14 — 2025-10-30 +# 🚀 Fleetbase v0.7.16 — 2025-11-06 -> Improved positions replay + meta field editors for drivers and vehicles” +> "New onboarding orchestrator, improved password security, UI improvements, bug fixes" --- ## ✨ Highlights -- Added ability to attach telematic devices to vehicles . -- Improved positions replay component to use client side + added step controls - Dropped `MovementTrackerService` from position playback components, use new `PositionPlaybackService` which implements full position playback completely on client side. -- Added pill components for driver, vehicle, device, and order. -- Fix custom fields manager component persistence https://github.com/fleetbase/ember-ui/pull/89 -- Improved dashboard isolation mechanism so that dashboard component can be rendered in multiple engines. -- Added meta viewer and editor for drivers, and vehicles. Fixes https://github.com/fleetbase/fleetbase/issues/440 +- Made the `LogApiRequests` middleware more robust +- Fixed controller validation handling +- Added microsoft365/graph mail driver +- Improved password requirements (including breached password check) +- Patched creating duplicate users by email in IAM +- Patch env mapper +- Vehicle/driver tracking API doesnt fire resource lifecycle events or log requests - only tracking events +- Patched `` component +- Security patch on Storefront customers API +- Styling updates on Storefront --- diff --git a/api/config/mail.php b/api/config/mail.php index bed5432c..b7e6d88b 100644 --- a/api/config/mail.php +++ b/api/config/mail.php @@ -66,6 +66,18 @@ return [ 'resend' => [], + 'microsoft-graph' => [ + 'transport' => 'microsoft-graph', + 'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'), + 'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'), + 'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'), + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@fleetbase.io'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Fleetbase')), + ], + 'save_to_sent_items' => env('MAIL_SAVE_TO_SENT_ITEMS', false), + ], + 'sendmail' => [ 'transport' => 'sendmail', 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'), diff --git a/console/app/components/configure/mail.hbs b/console/app/components/configure/mail.hbs index 5ba894ec..0fd0b9c8 100644 --- a/console/app/components/configure/mail.hbs +++ b/console/app/components/configure/mail.hbs @@ -13,6 +13,14 @@ {{/if}} + {{#if (eq this.mailer "microsoft-graph")}} + + + + + + + {{/if}} {{#if (eq this.mailer "mailgun")}} diff --git a/console/app/components/configure/mail.js b/console/app/components/configure/mail.js index 42643afc..504525cc 100644 --- a/console/app/components/configure/mail.js +++ b/console/app/components/configure/mail.js @@ -26,6 +26,10 @@ export default class ConfigureMailComponent extends Component { @tracked postmarkToken = null; @tracked sendgridApi_key = null; @tracked resendKey = null; + @tracked microsoftGraphClient_id = null; + @tracked microsoftGraphClient_secret = null; + @tracked microsoftGraphTenant_id = null; + @tracked microsoftGraphSave_to_sent_items = false; /** * Creates an instance of ConfigureFilesystemComponent. @@ -64,6 +68,19 @@ export default class ConfigureMailComponent extends Component { }; } + @action serializeMicrosoftGraphConfig() { + return { + client_id: this.microsoftGraphClient_id, + client_secret: this.microsoftGraphClient_secret, + tenant_id: this.microsoftGraphTenant_id, + save_to_sent_items: this.microsoftGraphSave_to_sent_items, + from: { + address: this.fromAddress, + name: this.fromName, + }, + }; + } + @action serializeMailgunConfig() { return { domain: this.mailgunDomain, @@ -112,6 +129,7 @@ export default class ConfigureMailComponent extends Component { postmark: this.serializePostmarkConfig(), sendgrid: this.serializeSendgridConfig(), resend: this.serializeResendConfig(), + microsoftGraph: this.serializeMicrosoftGraphConfig(), }); } catch (error) { this.notifications.serverError(error); @@ -131,6 +149,7 @@ export default class ConfigureMailComponent extends Component { postmark: this.serializePostmarkConfig(), sendgrid: this.serializeSendgridConfig(), resend: this.serializeResendConfig(), + microsoftGraph: this.serializeMicrosoftGraphConfig(), }); this.notifications.success('Mail configuration saved.'); } catch (error) { diff --git a/console/app/components/onboarding/form.hbs b/console/app/components/onboarding/form.hbs new file mode 100644 index 00000000..2f10fa3f --- /dev/null +++ b/console/app/components/onboarding/form.hbs @@ -0,0 +1,42 @@ +
+
+ {{t +
+

+ {{t "onboard.index.title"}} +

+
+
+ +
+
+ +
+

+ {{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}} + {{t "onboard.index.welcome-text"}} +

+
+ +
+ {{#if this.error}} + + {{/if}} + + + + + + + + + +
+
+ + + + + +
\ No newline at end of file diff --git a/console/app/components/onboarding/form.js b/console/app/components/onboarding/form.js new file mode 100644 index 00000000..00274c95 --- /dev/null +++ b/console/app/components/onboarding/form.js @@ -0,0 +1,77 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { action, getProperties } from '@ember/object'; +import { isBlank } from '@ember/utils'; +import { task } from 'ember-concurrency'; +import OnboardValidations from '../../validations/onboard'; +import lookupValidator from 'ember-changeset-validations'; +import Changeset from 'ember-changeset'; + +export default class OnboardingFormComponent extends Component { + @service fetch; + @service session; + @service router; + @service notifications; + @service urlSearchParams; + @tracked name; + @tracked email; + @tracked phone; + @tracked organization_name; + @tracked password; + @tracked password_confirmation; + @tracked error; + + get filled() { + // eslint-disable-next-line ember/no-get + const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation'); + return Object.values(input).every((val) => !isBlank(val)); + } + + @task *onboard(event) { + event?.preventDefault?.(); + + // eslint-disable-next-line ember/no-get + const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation'); + const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations); + + yield changeset.validate(); + + if (changeset.get('isInvalid')) { + const errorMessage = changeset.errors.firstObject.validation.firstObject; + + this.notifications.error(errorMessage); + return; + } + + // Set user timezone + input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + try { + const { status, skipVerification, token, session } = yield this.fetch.post('onboard/create-account', input); + if (status !== 'success') { + this.notifications.error('Onboard failed'); + return; + } + + // save session + this.args.context.persist('session', session); + + if (skipVerification === true && token) { + // only manually authenticate if skip verification + this.session.isOnboarding().manuallyAuthenticate(token); + + yield this.router.transitionTo('console'); + return this.notifications.success('Welcome to Fleetbase!'); + } else { + this.args.orchestrator.next(); + this.urlSearchParams.setParamsToCurrentUrl({ + step: this.args.orchestrator?.current?.id, + session, + }); + } + } catch (err) { + this.notifications.serverError(err); + } + } +} diff --git a/console/app/components/onboarding/verify-email.hbs b/console/app/components/onboarding/verify-email.hbs new file mode 100644 index 00000000..92017938 --- /dev/null +++ b/console/app/components/onboarding/verify-email.hbs @@ -0,0 +1,78 @@ +{{page-title (t "onboard.verify-email.header-title")}} + +{{#if this.initialized}} +
+
+ + + +

+ {{t "onboard.verify-email.title"}} +

+
+ + + {{t "onboard.verify-email.message-text" htmlSafe=true}} + + +
+ + + + + {{#if this.verification.waiting}} +
+
+
+ +
+
+
+
{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}
+
{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}
+
+
+
+
+
+
+ {{/if}} + +
+{{/if}} \ No newline at end of file diff --git a/console/app/components/onboarding/verify-email.js b/console/app/components/onboarding/verify-email.js new file mode 100644 index 00000000..72636d77 --- /dev/null +++ b/console/app/components/onboarding/verify-email.js @@ -0,0 +1,53 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { later, next } from '@ember/runloop'; +import { not } from '@ember/object/computed'; +import { task } from 'ember-concurrency'; + +export default class OnboardingVerifyEmailComponent extends Component { + @service('session') authSession; + @service('user-verification') verification; + @service fetch; + @service notifications; + @service router; + @service urlSearchParams; + @tracked code; + @tracked session; + @tracked initialized = false; + + constructor() { + super(...arguments); + next(() => this.#initialize()); + } + + #initialize() { + this.code = this.urlSearchParams.get('code'); + this.session = this.args.context.get('session') ?? this.urlSearchParams.get('session'); + this.initialized = true; + this.verification.start(); + } + + @task *verify(event) { + event?.preventDefault?.(); + + try { + const { status, token } = yield this.fetch.post('onboard/verify-email', { session: this.session, code: this.code }); + if (status === 'ok') { + this.notifications.success('Email successfully verified!'); + + if (token) { + this.notifications.info('Welcome to Fleetbase!'); + this.authSession.manuallyAuthenticate(token); + + return this.router.transitionTo('console'); + } + + return this.router.transitionTo('auth.login'); + } + } catch (error) { + this.notifications.serverError(error); + } + } +} diff --git a/console/app/components/onboarding/yield.hbs b/console/app/components/onboarding/yield.hbs new file mode 100644 index 00000000..f75a6146 --- /dev/null +++ b/console/app/components/onboarding/yield.hbs @@ -0,0 +1,11 @@ +
+ {{#if this.initialized}} + {{#if this.currentComponent}} + {{component this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}} + {{/if}} + {{else}} +
+ +
+ {{/if}} +
\ No newline at end of file diff --git a/console/app/components/onboarding/yield.js b/console/app/components/onboarding/yield.js new file mode 100644 index 00000000..cf4af1ba --- /dev/null +++ b/console/app/components/onboarding/yield.js @@ -0,0 +1,27 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { next } from '@ember/runloop'; + +export default class OnboardingYieldComponent extends Component { + @service('onboarding-orchestrator') orchestrator; + @service('onboarding-context') context; + @tracked initialized = false; + + get currentComponent() { + return this.orchestrator.current && this.orchestrator.current.component; + } + + constructor(owner, { step, session, code }) { + super(...arguments); + next(() => this.#initialize(step, session, code)); + } + + #initialize(step, session, code) { + if (step) this.orchestrator.goto(step); + if (session) this.context.persist('session', session); + if (code) this.context.set('code', code); + + this.initialized = true; + } +} diff --git a/console/app/controllers/onboard/index.js b/console/app/controllers/onboard/index.js index d276c325..5191313c 100644 --- a/console/app/controllers/onboard/index.js +++ b/console/app/controllers/onboard/index.js @@ -1,151 +1,8 @@ import Controller from '@ember/controller'; -import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; -import { action, getProperties } from '@ember/object'; -import OnboardValidations from '../../validations/onboard'; -import lookupValidator from 'ember-changeset-validations'; -import Changeset from 'ember-changeset'; export default class OnboardIndexController extends Controller { - /** - * Inject the `fetch` service - * - * @memberof OnboardIndexController - */ - @service fetch; - - /** - * Inject the `session` service - * - * @memberof OnboardIndexController - */ - @service session; - - /** - * Inject the `router` service - * - * @memberof OnboardIndexController - */ - @service router; - - /** - * Inject the `notifications` service - * - * @memberof OnboardIndexController - */ - @service notifications; - - /** - * The name input field. - * - * @memberof OnboardIndexController - */ - @tracked name; - - /** - * The email input field. - * - * @memberof OnboardIndexController - */ - @tracked email; - - /** - * The phone input field. - * - * @memberof OnboardIndexController - */ - @tracked phone; - - /** - * The organization_name input field. - * - * @memberof OnboardIndexController - */ - @tracked organization_name; - - /** - * The password input field. - * - * @memberof OnboardIndexController - */ - @tracked password; - - /** - * The name password confirmation field. - * - * @memberof OnboardIndexController - */ - @tracked password_confirmation; - - /** - * The property for error message. - * - * @memberof OnboardIndexController - */ - @tracked error; - - /** - * The loading state of the onboard request. - * - * @memberof OnboardIndexController - */ - @tracked isLoading = false; - - /** - * The ready state for the form. - * - * @memberof OnboardIndexController - */ - @tracked readyToSubmit = false; - - /** - * Start the onboard process. - * - * @return {Promise} - * @memberof OnboardIndexController - */ - @action async startOnboard(event) { - event.preventDefault(); - - // eslint-disable-next-line ember/no-get - const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation'); - const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations); - - await changeset.validate(); - - if (changeset.get('isInvalid')) { - const errorMessage = changeset.errors.firstObject.validation.firstObject; - - this.notifications.error(errorMessage); - return; - } - - // Set user timezone - input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - - this.isLoading = true; - - return this.fetch - .post('onboard/create-account', input) - .then(({ status, skipVerification, token, session }) => { - if (status === 'success') { - if (skipVerification === true && token) { - // only manually authenticate if skip verification - this.session.isOnboarding().manuallyAuthenticate(token); - - return this.router.transitionTo('console').then(() => { - this.notifications.success('Welcome to Fleetbase!'); - }); - } - - return this.router.transitionTo('onboard.verify-email', { queryParams: { hello: session } }); - } - }) - .catch((error) => { - this.notifications.serverError(error); - }) - .finally(() => { - this.isLoading = false; - }); - } + @tracked step; + @tracked session; + @tracked code; } diff --git a/console/app/instance-initializers/register-default-onboarding-flow.js b/console/app/instance-initializers/register-default-onboarding-flow.js new file mode 100644 index 00000000..17280e01 --- /dev/null +++ b/console/app/instance-initializers/register-default-onboarding-flow.js @@ -0,0 +1,19 @@ +export function initialize(owner) { + const registry = owner.lookup('service:onboarding-registry'); + if (registry) { + const defaultFlow = { + id: 'default@v1', + entry: 'signup', + steps: [ + { id: 'signup', component: 'onboarding/form', next: 'verify-email' }, + { id: 'verify-email', component: 'onboarding/verify-email' }, + ], + }; + + registry.registerFlow(defaultFlow); + } +} + +export default { + initialize, +}; diff --git a/console/app/routes/onboard/index.js b/console/app/routes/onboard/index.js index 2cf6b08b..16e4b55a 100644 --- a/console/app/routes/onboard/index.js +++ b/console/app/routes/onboard/index.js @@ -3,6 +3,17 @@ import { inject as service } from '@ember/service'; export default class OnboardIndexRoute extends Route { @service store; + @service('onboarding-orchestrator') orchestrator; + + queryParams = { + step: { refreshModel: false }, + session: { refreshModel: false }, + code: { refreshModel: false }, + }; + + beforeModel() { + this.orchestrator.start(); + } model() { return this.store.findRecord('brand', 1); diff --git a/console/app/services/onboarding-context.js b/console/app/services/onboarding-context.js new file mode 100644 index 00000000..d9ec038b --- /dev/null +++ b/console/app/services/onboarding-context.js @@ -0,0 +1,39 @@ +import Service, { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +export default class OnboardingContextService extends Service { + @service appCache; + @tracked data = {}; + + get(key) { + return this.data[key] ?? this.appCache.get(`onboarding:context:${key}`); + } + + getFromCache(key) { + return this.appCache.get(`onboarding:context:${key}`); + } + + set(key, value, options = {}) { + this.data = { ...this.data, [key]: value }; + if (options?.persist === true) { + this.appCache.set(`onboarding:context:${key}`, value); + } + } + + persist(key, value) { + this.set(key, value, { persist: true }); + } + + del(key) { + const { [key]: _drop, ...rest } = this.data; // eslint-disable-line no-unused-vars + this.data = rest; + this.appCache.set(`onboarding:context:${key}`, undefined); + } + + reset() { + for (let key in this.data) { + this.appCache.set(`onboarding:context:${key}`, undefined); + } + this.data = {}; + } +} diff --git a/console/app/services/onboarding-orchestrator.js b/console/app/services/onboarding-orchestrator.js new file mode 100644 index 00000000..b7793b73 --- /dev/null +++ b/console/app/services/onboarding-orchestrator.js @@ -0,0 +1,71 @@ +import Service from '@ember/service'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +export default class OnboardingOrchestratorService extends Service { + @service onboardingRegistry; + @service onboardingContext; + + @tracked flow = null; + @tracked current = null; + @tracked history = []; + @tracked sessionId = null; + + start(flowId = null, opts = {}) { + const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow); + if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`); + this.flow = flow; + this.sessionId = opts.sessionId || null; + this.history = []; + this.goto(flow.entry); + } + + async goto(stepId) { + if (!this.flow) throw new Error('No active onboarding flow'); + const step = this.flow.steps.find((s) => s.id === stepId); + if (!step) throw new Error(`Step '${stepId}' not found`); + + if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) { + return this.next(); + } + + if (typeof step.beforeEnter === 'function') { + await step.beforeEnter(this.onboardingContext); + } + + this.current = step; + } + + async next() { + if (!this.flow || !this.current) return; + + const leaving = this.current; + if (typeof leaving.afterLeave === 'function') { + await leaving.afterLeave(this.onboardingContext); + } + + if (!this.history.includes(leaving)) this.history.push(leaving); + + let nextId; + if (typeof leaving.next === 'function') { + nextId = leaving.next(this.onboardingContext); + } else { + nextId = leaving.next; + } + + if (!nextId) { + this.current = null; // finished + return; + } + + return this.goto(nextId); + } + + async back() { + if (!this.flow || this.history.length === 0) return; + const prev = this.history[this.history.length - 1]; + if (prev && prev.allowBack === false) return; + this.history = this.history.slice(0, -1); + await this.goto(prev.id); + } +} diff --git a/console/app/services/onboarding-registry.js b/console/app/services/onboarding-registry.js new file mode 100644 index 00000000..8b8d6313 --- /dev/null +++ b/console/app/services/onboarding-registry.js @@ -0,0 +1,31 @@ +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +export default class OnboardingRegistryService extends Service { + flows = new Map(); + @tracked defaultFlow = 'default@v1'; + + useFlow(flowId) { + this.defaultFlow = flowId; + } + + registerFlow(flow) { + if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) { + throw new Error('Invalid FlowDef: id, entry, steps are required'); + } + const ids = new Set(flow.steps.map((s) => s.id)); + if (!ids.has(flow.entry)) { + throw new Error(`Flow '${flow.id}' entry '${flow.entry}' not found in steps`); + } + for (const s of flow.steps) { + if (typeof s.next === 'string' && s.next && !ids.has(s.next)) { + throw new Error(`Flow '${flow.id}' step '${s.id}' has unknown next '${s.next}'`); + } + } + this.flows.set(flow.id, flow); + } + + getFlow(id) { + return this.flows.get(id); + } +} diff --git a/console/app/services/user-verification.js b/console/app/services/user-verification.js new file mode 100644 index 00000000..3f33c6c7 --- /dev/null +++ b/console/app/services/user-verification.js @@ -0,0 +1,114 @@ +import Service, { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { later } from '@ember/runloop'; +import { task } from 'ember-concurrency'; + +export default class UserVerificationService extends Service { + @service fetch; + @service notifications; + @service modalsManager; + @service currentUser; + @service router; + @service session; + @service intl; + @tracked token; + @tracked code; + @tracked ready; + @tracked waiting = false; + + @action start(options = {}) { + this.#wait(options?.timeout ?? 75000); + } + + @action didntReceiveCode() { + this.waiting = true; + } + + @action validateInput(event) { + const value = event instanceof HTMLElement ? event.value : (event?.target?.value ?? ''); + this.ready = value?.length > 5; + } + + @action resendBySms() { + this.modalsManager.show('modals/verify-by-sms', { + title: 'Verify Account by Phone', + acceptButtonText: 'Send', + phone: this.currentUser.phone, + confirm: async (modal) => { + modal.startLoading(); + const phone = modal.getOption('phone'); + if (!phone) { + this.notifications.error('No phone number provided.'); + } + + try { + await this.fetch.post('onboard/send-verification-sms', { phone, session: this.hello }); + this.notifications.success('Verification code SMS sent!'); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, + }); + } + + @action resendEmail() { + this.modalsManager.show('modals/resend-verification-email', { + title: 'Resend Verification Code', + acceptButtonText: 'Send', + email: this.currentUser.email, + confirm: async (modal) => { + modal.startLoading(); + const email = modal.getOption('email'); + if (!email) { + this.notifications.error('No email number provided.'); + } + + try { + await this.fetch.post('onboard/send-verification-email', { email, session: this.hello }); + this.notifications.success('Verification code email sent!'); + modal.done(); + } catch (error) { + this.notifications.serverError(error); + modal.stopLoading(); + } + }, + }); + } + + @task *verifyCode() { + try { + const { status, token } = yield this.fetch.post('auth/verify-email', { token: this.token, code: this.code, email: this.email, authenticate: true }); + if (status === 'ok') { + this.notifications.success('Email successfully verified!'); + + if (token) { + this.notifications.info(`Welcome to ${this.intl.t('app.name')}`); + this.session.manuallyAuthenticate(token); + + return this.router.transitionTo('console'); + } + + return this.router.transitionTo('auth.login'); + } + } catch (error) { + this.notifications.serverError(error); + } + } + + setToken(token) { + this.token = token; + } + + setCode(code) { + this.code = code; + } + + #wait(timeout = 75000) { + return later(this, () => { + this.waiting = true; + }, timeout); + } +} diff --git a/console/app/templates/onboard/index.hbs b/console/app/templates/onboard/index.hbs index a1c78dbf..74d4e120 100644 --- a/console/app/templates/onboard/index.hbs +++ b/console/app/templates/onboard/index.hbs @@ -1,42 +1 @@ -
-
- {{t -
-

- {{t "onboard.index.title"}} -

-
-
- -
-
- -
-

- {{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}} - {{t "onboard.index.welcome-text"}} -

-
- -
- {{#if this.error}} - - {{/if}} - - - - - - - - - -
-
- - - - - -
\ No newline at end of file + \ No newline at end of file diff --git a/console/router.map.js b/console/router.map.js index fa6e9026..60c65b67 100644 --- a/console/router.map.js +++ b/console/router.map.js @@ -9,9 +9,7 @@ export default class Router extends EmberRouter { Router.map(function () { this.route('virtual', { path: '/:slug' }); this.route('install'); - this.route('onboard', function () { - this.route('verify-email'); - }); + this.route('onboard'); this.route('auth', function () { this.route('login', { path: '/' }); this.route('forgot-password'); diff --git a/console/tests/integration/components/onboarding/form-test.js b/console/tests/integration/components/onboarding/form-test.js new file mode 100644 index 00000000..b43f7eb2 --- /dev/null +++ b/console/tests/integration/components/onboarding/form-test.js @@ -0,0 +1,26 @@ +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 | onboarding/form', 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``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/console/tests/integration/components/onboarding/verify-email-test.js b/console/tests/integration/components/onboarding/verify-email-test.js new file mode 100644 index 00000000..f1b7067d --- /dev/null +++ b/console/tests/integration/components/onboarding/verify-email-test.js @@ -0,0 +1,26 @@ +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 | onboarding/verify-email', 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``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/console/tests/integration/components/onboarding/yield-test.js b/console/tests/integration/components/onboarding/yield-test.js new file mode 100644 index 00000000..0be6b22e --- /dev/null +++ b/console/tests/integration/components/onboarding/yield-test.js @@ -0,0 +1,26 @@ +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 | onboarding/yield', 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``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/console/tests/unit/instance-initializers/register-default-onboarding-flow-test.js b/console/tests/unit/instance-initializers/register-default-onboarding-flow-test.js new file mode 100644 index 00000000..9157d5fb --- /dev/null +++ b/console/tests/unit/instance-initializers/register-default-onboarding-flow-test.js @@ -0,0 +1,39 @@ +import Application from '@ember/application'; + +import config from '@fleetbase/console/config/environment'; +import { initialize } from '@fleetbase/console/instance-initializers/register-default-onboarding-flow'; +import { module, test } from 'qunit'; +import Resolver from 'ember-resolver'; +import { run } from '@ember/runloop'; + +module('Unit | Instance Initializer | register-default-onboarding-flow', 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); + }); +}); diff --git a/console/tests/unit/services/onboarding-context-test.js b/console/tests/unit/services/onboarding-context-test.js new file mode 100644 index 00000000..a4e34ee4 --- /dev/null +++ b/console/tests/unit/services/onboarding-context-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from '@fleetbase/console/tests/helpers'; + +module('Unit | Service | onboarding-context', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:onboarding-context'); + assert.ok(service); + }); +}); diff --git a/console/tests/unit/services/onboarding-orchestrator-test.js b/console/tests/unit/services/onboarding-orchestrator-test.js new file mode 100644 index 00000000..606a7701 --- /dev/null +++ b/console/tests/unit/services/onboarding-orchestrator-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from '@fleetbase/console/tests/helpers'; + +module('Unit | Service | onboarding-orchestrator', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:onboarding-orchestrator'); + assert.ok(service); + }); +}); diff --git a/console/tests/unit/services/onboarding-registry-test.js b/console/tests/unit/services/onboarding-registry-test.js new file mode 100644 index 00000000..ffd50099 --- /dev/null +++ b/console/tests/unit/services/onboarding-registry-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from '@fleetbase/console/tests/helpers'; + +module('Unit | Service | onboarding-registry', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:onboarding-registry'); + assert.ok(service); + }); +}); diff --git a/console/tests/unit/services/user-verification-test.js b/console/tests/unit/services/user-verification-test.js new file mode 100644 index 00000000..cba34e9b --- /dev/null +++ b/console/tests/unit/services/user-verification-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from '@fleetbase/console/tests/helpers'; + +module('Unit | Service | user-verification', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:user-verification'); + assert.ok(service); + }); +}); diff --git a/docker/Dockerfile b/docker/Dockerfile index 78f2ae69..e0cdffd0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -75,7 +75,7 @@ ENV QUEUE_CONNECTION=redis ENV CADDYFILE_PATH=/fleetbase/Caddyfile ENV CONSOLE_PATH=/fleetbase/console ENV OCTANE_SERVER=frankenphp -ENV FLEETBASE_VERSION=0.7.14 +ENV FLEETBASE_VERSION=0.7.16 # Set environment ARG ENVIRONMENT=production diff --git a/packages/core-api b/packages/core-api index 0dd2ef76..70937035 160000 --- a/packages/core-api +++ b/packages/core-api @@ -1 +1 @@ -Subproject commit 0dd2ef7648ed834d5f9fa14f6f57906821ee0395 +Subproject commit 7093703521e6915aab6ad93afb4997eb6e9559c3 diff --git a/packages/ember-ui b/packages/ember-ui index 052a8442..cb43182a 160000 --- a/packages/ember-ui +++ b/packages/ember-ui @@ -1 +1 @@ -Subproject commit 052a8442c21e12b07fe437c7dc6e86ba2019c690 +Subproject commit cb43182a11c24175554916575fba01a2edaa9d57 diff --git a/packages/fleetops b/packages/fleetops index 0c987e2a..8d5f1f37 160000 --- a/packages/fleetops +++ b/packages/fleetops @@ -1 +1 @@ -Subproject commit 0c987e2acdfe7aef6bac8dc2034827688cc070d2 +Subproject commit 8d5f1f3707236b13d6a43b7a1131e644a446bc71 diff --git a/packages/storefront b/packages/storefront index 3c86e4f8..ede69c8c 160000 --- a/packages/storefront +++ b/packages/storefront @@ -1 +1 @@ -Subproject commit 3c86e4f8ad8f15ade883967a498990428ee0a0fa +Subproject commit ede69c8c3875c451a68b26207214108e9bea8d6e