Fixed 2FA Config component

This commit is contained in:
TemuulenBM
2024-01-03 17:58:17 +08:00
parent d314964776
commit 850cc1e20d
12 changed files with 312 additions and 114 deletions

View File

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

View File

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

View File

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

View File

@@ -1,44 +1,116 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { inject as service } from '@ember/service';
/**
* Default Two-Factor Authentication method when not explicitly selected.
*
* @property {string} DEFAULT_2FA_METHOD
* @private
*/
const DEFAULT_2FA_METHOD = 'authenticator_app';
/**
* Glimmer component for managing Two-Factor Authentication settings.
*
* @class TwoFaSettingsComponent
* @extends Component
*/
export default class TwoFaSettingsComponent extends Component {
/**
* The fetch service for making HTTP requests.
*
* @property {Service} fetch
* @public
*/
@service fetch;
/**
* The notifications service for displaying user notifications.
*
* @property {Service} notifications
* @public
*/
@service notifications;
@tracked selected2FAMethod;
@tracked is2FAEnabled;
@tracked twoFaSettings;
@tracked isLoading;
/**
* The currently selected Two-Factor Authentication method.
*
* @property {string} selectedTwoFaMethod
* @public
*/
@tracked selectedTwoFaMethod;
TwoFaMethods = [
{ name: 'Authenticator App', description: 'Get codes from an app like Authy, 1Password, Microsoft Authenticator, or Google Authenticator', recommended: true },
{ name: 'SMS', description: 'Receive a unique code via SMS' },
{ name: 'Email', description: 'Receive a unique code via Email' },
];
/**
* Indicates whether Two-Factor Authentication is currently enabled.
*
* @property {boolean} isTwoFaEnabled
* @public
*/
@tracked isTwoFaEnabled;
constructor() {
/**
* Class constructor to initialize the component.
*
* @constructor
* @param {Object} owner - The owner of the component.
* @param {Object} options - Options passed during component instantiation.
* @param {Object} options.twoFaSettings - The current Two-Factor Authentication settings.
* @param {Array} options.twoFaMethods - Available Two-Factor Authentication methods.
*/
constructor(owner, { twoFaSettings, twoFaMethods }) {
super(...arguments);
if (this.is2FAEnabled) {
this.selected2FAMethod = this.TwoFaMethods.find((method) => method.recommended);
}
const userSelectedMethod = isArray(twoFaMethods) ? twoFaMethods.find(({ key }) => key === twoFaSettings.method) : null;
this.isTwoFaEnabled = twoFaSettings.enabled === true;
this.selectedTwoFaMethod = userSelectedMethod ? userSelectedMethod.key : DEFAULT_2FA_METHOD;
}
@action async toggle2FA() {
this.is2FAEnabled = !this.is2FAEnabled;
/**
* Action handler for toggling Two-Factor Authentication.
*
* @method onTwoFaToggled
* @param {boolean} isTwoFaEnabled - Indicates whether Two-Factor Authentication is enabled.
* @return {void}
* @public
*/
@action onTwoFaToggled(isTwoFaEnabled) {
this.isTwoFaEnabled = isTwoFaEnabled;
if (!this.is2FAEnabled) {
this.selected2FAMethod = null;
if (isTwoFaEnabled) {
const recommendedMethod = isArray(this.args.twoFaMethods) ? this.args.twoFaMethods.find((method) => method.recommended) : null;
if (recommendedMethod) {
this.selectedTwoFaMethod = recommendedMethod.key;
}
} else {
this.selected2FAMethod = this.TwoFaMethods.find((method) => method.recommended);
this.selectedTwoFaMethod = null;
}
console.log('selected2FAMethod:', this.selected2FAMethod);
if (typeof this.args.onTwoFaToggled === 'function') {
this.args.onTwoFaToggled(...arguments);
}
if (typeof this.args.onTwoFaMethodSelected === 'function') {
this.args.onTwoFaMethodSelected(this.selectedTwoFaMethod);
}
}
@action select2FAMethod(method) {
this.selected2FAMethod = method;
console.log('selected2FAMethod:', this.selected2FAMethod);
/**
* Action handler for selecting a Two-Factor Authentication method.
*
* @method onTwoFaSelected
* @param {string} method - The selected Two-Factor Authentication method.
* @return {void}
* @public
*/
@action onTwoFaSelected(method) {
this.selectedTwoFaMethod = method;
if (typeof this.args.onTwoFaMethodSelected === 'function') {
this.args.onTwoFaMethodSelected(...arguments);
}
}
}

View File

@@ -11,7 +11,7 @@ export default class AuthLoginController extends Controller {
* @var {Controller}
*/
@controller('auth.forgot-password') forgotPasswordController;
/**
* Inject the `notifications` service
*
@@ -89,45 +89,50 @@ export default class AuthLoginController extends Controller {
*/
@tracked failedAttempts = 0;
@action async login(event) {
// firefox patch
event.preventDefault();
// get user credentials
const { identity, password, rememberMe } = this;
// start loader
this.set('isLoading', true);
// set where to redirect on login
this.setRedirect();
try {
await this.session.authenticate('authenticator:fleetbase', { identity, password }, rememberMe);
} catch (error) {
this.failedAttempts++;
return this.failure(error);
}
if (this.session.isAuthenticated) {
this.success();
}
}
/**
* Authenticate the user
*
* @void
*/
@action async login(event) {
// firefox patch
event.preventDefault();
// get user credentials
const { email, password, rememberMe } = this;
// start loader
this.set('isLoading', true);
// set where to redirect on login
this.setRedirect();
try {
await this.session.authenticate('authenticator:fleetbase', { email, password }, rememberMe);
const isTwoFactorEnabled = this.session.user ? this.session.user.isTwoFactorEnabled : false;
if (isTwoFactorEnabled) {
// Redirect to the 2FA page if 2FA is enabled
this.transitionToRoute('auth.two-fa');
} else {
// Continue with the default behavior (e.g., redirect to another route)
this.success();
}
} catch (error) {
this.failedAttempts++;
return this.failure(error);
}
if (this.session.isAuthenticated) {
this.success();
}
}
// @action async login(event) {
// // firefox patch
// event.preventDefault();
// // get user credentials
// const { email, password, rememberMe } = this;
// // start loader
// this.set('isLoading', true);
// // set where to redirect on login
// this.setRedirect();
// try {
// await this.session.authenticate('authenticator:fleetbase', { email, password }, rememberMe);
// } catch (error) {
// this.failedAttempts++;
// return this.failure(error);
// }
// if (this.session.isAuthenticated) {
// this.success();
// }
// }
/**
* Transition user to onboarding screen

View File

@@ -1,3 +1,11 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class AuthTwoFaController extends Controller {
@action verifyCode() {
// console.log('Verification code submitted!');
}
export default class AuthTwoFaController extends Controller {}
@action resendCode() {
// console.log('Resending verification code...');
}
}

View File

@@ -3,6 +3,12 @@ import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
/**
* Controller responsible for handling Two-Factor Authentication settings in the admin console.
*
* @class ConsoleAdminTwoFaSettingsController
* @extends Controller
*/
export default class ConsoleAdminTwoFaSettingsController extends Controller {
/**
* Inject the notifications service.
@@ -18,9 +24,30 @@ export default class ConsoleAdminTwoFaSettingsController extends Controller {
*/
@service fetch;
@tracked selected2FAMethod = null;
/**
* Tracked property for the Two-Factor Authentication enabled state.
*
* @memberof ConsoleAdminTwoFaSettingsController
* @type {Boolean}
*/
@tracked twoFaMethod = 'authenticator_app';
@tracked is2FAEnabled = true;
/**
* Array of available Two-Factor Authentication methods.
*
* @memberof ConsoleAdminTwoFaSettingsController
* @type {Array}
*/
@tracked methods = [
{
key: 'authenticator_app',
name: 'Authenticator App',
description: 'Get codes from an app like Authy, 1Password, Microsoft Authenticator, or Google Authenticator',
recommended: true,
},
{ key: 'sms', name: 'SMS', description: 'Receive a unique code via SMS' },
{ key: 'email', name: 'Email', description: 'Receive a unique code via Email' },
];
/**
* The 2FA settings value JSON.
@@ -29,8 +56,8 @@ export default class ConsoleAdminTwoFaSettingsController extends Controller {
* @var {Object}
*/
@tracked twoFaSettings = {
Selected: this.selected2FAMethod,
Enabled: this.is2FAEnabled,
enabled: false,
method: 'authenticator_app',
};
/**
@@ -41,9 +68,54 @@ export default class ConsoleAdminTwoFaSettingsController extends Controller {
*/
@tracked isLoading = false;
/**
* Constructor method for the controller.
*
* @constructor
*/
constructor() {
super(...arguments);
this.twoFaSettings;
this.loadSettings();
}
/**
* Action method triggered when Two-Factor Authentication is toggled.
*
* @action
* @param {Boolean} isEnabled - The new state of Two-Factor Authentication.
*/
@action onTwoFaToggled(isEnabled) {
this.isTwoFaEnabled = isEnabled;
}
/**
* Action method triggered when a Two-Factor Authentication method is selected.
*
* @action
* @param {String} method - The selected Two-Factor Authentication method.
*/
@action onTwoFaMethodSelected(method) {
this.twoFaMethod = method;
}
/**
* Action method to load Two-Factor Authentication settings from the server.
*
* @action
* @returns {Promise}
*/
@action loadSettings() {
return this.fetch
.get('two-fa/settings')
.then((twoFaSettings) => {
this.twoFaSettings = twoFaSettings;
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
/**
@@ -55,16 +127,17 @@ export default class ConsoleAdminTwoFaSettingsController extends Controller {
* @memberof ConsoleAdminTwoFaSettingsController
*/
@action saveSettings() {
const { twoFaSettings } = this;
console.log('settings', twoFaSettings);
const twoFaSettings = {
enabled: this.isTwoFaEnabled,
method: this.twoFaMethod,
};
this.isLoading = true;
return this.fetch
.post('two-fa-settings/save-settings', { twoFaSettings })
.post('two-fa/settings', { twoFaSettings })
.then(() => {
this.notifications.success('2FA settings successfully saved.');
this.notifications.success('2FA Settings saved successfully.');
})
.catch((error) => {
this.notifications.serverError(error);

View File

@@ -11,7 +11,6 @@ Router.map(function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password');
this.route('two-fa');
});
this.route('onboard', function () {
this.route('verify-email');
@@ -53,16 +52,16 @@ Router.map(function () {
path: 'developers'
});
this.mount('@fleetbase/fleetops-engine', {
as: 'fleet-ops',
path: 'fleet-ops'
});
this.mount('@fleetbase/iam-engine', {
as: 'iam',
path: 'iam'
});
this.mount('@fleetbase/fleetops-engine', {
as: 'fleet-ops',
path: 'fleet-ops'
});
this.mount('@fleetbase/storefront-engine', {
as: 'storefront',
path: 'storefront'

View File

@@ -1,3 +1,22 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class AuthTwoFaRoute extends Route {}
export default class AuthTwoFaRoute extends Route {
@service fetch;
queryParams = {
token: {
refreshModel: false
}
}
beforeModel(transition) {
// validate 2fa session with server
const { token } = transition.params;
return this.fetch.post('two-fa/validate-session', { token }).catch((error) => {
this.notifications.serverError(error);
return this.redirect('auth.login');
});
}
}

View File

@@ -14,7 +14,8 @@
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-900 mr-4" />
</div>
<p class="flex-1 text-sm text-yellow-900 dark:yellow-red-900">
<strong>Forgot your password?</strong><br> Click the button below to reset your password.
<strong>Forgot your password?</strong><br />
Click the button below to reset your password.
</p>
</div>
<Button @text="Ok, help me reset!" @type="warning" @onClick={{this.forgotPassword}} />
@@ -24,11 +25,44 @@
<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.email}}
aria-label="Email address"
name="email"
@type="email"
autocomplete="username"
required
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
placeholder="Email address"
disabled={{this.isLoading}}
/>
</div> --}}
<div>
<Input @value={{this.email}} aria-label="Email address" name="email" @type="email" autocomplete="username" required class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900" placeholder="Email address" disabled={{this.isLoading}} />
<Input
@value={{this.identity}}
aria-label="Email or Phone Number"
name="email"
@type="email"
autocomplete="username"
required
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
placeholder="Email or Phone Number"
disabled={{this.isLoading}}
/>
</div>
<div class="-mt-px">
<Input @value={{this.password}} aria-label="Password" name="password" @type="password" autocomplete="current-password" required class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900" placeholder="Password" disabled={{this.isLoading}} />
<Input
@value={{this.password}}
aria-label="Password"
name="password"
@type="password"
autocomplete="current-password"
required
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
placeholder="Password"
disabled={{this.isLoading}}
/>
</div>
</div>
@@ -41,7 +75,12 @@
</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">
<a
href="javascript:;"
{{on "click" this.forgotPassword}}
disabled={{this.isLoading}}
class="font-medium transition duration-150 ease-in-out text-sky-600 hover:text-sky-500 focus:outline-none focus:underline"
>
Forgot your password?
</a>
</div>

View File

@@ -16,19 +16,6 @@
</p>
</div>
<div class="mb-4">
<label for="mfaMethod" class="block text-sm font-medium text-gray-700 dark:text-gray-50">
Choose 2FA Method
</label>
<div class="mt-2">
<select id="mfaMethod" name="mfaMethod" class="form-select form-select-lg w-full" {{on "change" this.updateSelectedMethod}}>
<option value="sms">SMS</option>
<option value="email">Email</option>
<option value="authenticator">Authenticator App</option>
</select>
</div>
</div>
<form class="space-y-6" {{on "submit" this.verifyCode}}>
<div>
<label for="verificationCode" class="block text-sm font-medium text-gray-700 dark:text-gray-50">
@@ -53,13 +40,6 @@
</a>
</div>
<div class="flex items-center">
<input type="checkbox" id="rememberDevice" name="rememberDevice" class="form-checkbox" checked={{this.rememberDevice}} {{on "change" this.toggleRememberDevice}} />
<label for="rememberDevice" class="ml-2 text-sm text-gray-700 dark:text-gray-50">
Remember this device
</label>
</div>
<div class="flex flex-row space-x-2">
<Button @icon="check-circle" @type="primary" @buttonType="submit" @text="Verify Code" @onClick={{this.verifyCode}} @isLoading={{this.isLoading}} />
<Button @buttonType="button" @text="Cancel" @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />

View File

@@ -1,18 +1,17 @@
{{page-title "Two-Factor Authentication"}}
<Layout::Section::Header @title="Two-Factor Authentication">
{{page-title "2FA Config"}}
<Layout::Section::Header @title="2FA Config">
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
</Layout::Section::Header>
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 1200}}>
<div class="max-w-3xl my-10 mx-auto space-y-4">
<ContentPanel @title="Two-Factor Authentication Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="2FA Config" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<TwoFaSettings
@selected2FAMethod={{this.selected2FAMethod}}
@is2FAEnabled={{this.is2FAEnabled}}
@twoFaMethods={{this.methods}}
@twoFaSettings={{this.twoFaSettings}}
@isLoading={{this.isLoading}}
@saveSettings={{this.saveSettings}}
@onTwoFaToggled={{this.onTwoFaToggled}}
@onTwoFaMethodSelected={{this.onTwoFaMethodSelected}}
/>
</ContentPanel>
</div>