diff --git a/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings b/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings
new file mode 100644
index 0000000000000..778ccde6854ed
--- /dev/null
+++ b/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Adds Account Protection requests
diff --git a/projects/js-packages/api/index.jsx b/projects/js-packages/api/index.jsx
index 8233d0ba8a616..6f6cdffe0b325 100644
--- a/projects/js-packages/api/index.jsx
+++ b/projects/js-packages/api/index.jsx
@@ -510,6 +510,16 @@ function JetpackRestApiClient( root, nonce ) {
getRequest( `${ wpcomOriginApiUrl }jetpack/v4/search/stats`, getParams )
.then( checkStatus )
.then( parseJsonResponse ),
+ fetchAccountProtectionSettings: () =>
+ getRequest( `${ apiRoot }jetpack/v4/account-protection`, getParams )
+ .then( checkStatus )
+ .then( parseJsonResponse ),
+ updateAccountProtectionSettings: newSettings =>
+ postRequest( `${ apiRoot }jetpack/v4/account-protection`, postParams, {
+ body: JSON.stringify( newSettings ),
+ } )
+ .then( checkStatus )
+ .then( parseJsonResponse ),
fetchWafSettings: () =>
getRequest( `${ apiRoot }jetpack/v4/waf`, getParams )
.then( checkStatus )
diff --git a/projects/packages/account-protection/changelog/add-jetpack-account-protection-security-settings b/projects/packages/account-protection/changelog/add-jetpack-account-protection-security-settings
new file mode 100644
index 0000000000000..af516388c3c6c
--- /dev/null
+++ b/projects/packages/account-protection/changelog/add-jetpack-account-protection-security-settings
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Adds handling for module activation and deactivation
diff --git a/projects/packages/account-protection/changelog/add-packages-account-protection-password-detection-flow b/projects/packages/account-protection/changelog/add-packages-account-protection-password-detection-flow
new file mode 100644
index 0000000000000..dde7e7363b212
--- /dev/null
+++ b/projects/packages/account-protection/changelog/add-packages-account-protection-password-detection-flow
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Adds the password detection flow
diff --git a/projects/packages/account-protection/changelog/add-protect-account-protection-settings b/projects/packages/account-protection/changelog/add-protect-account-protection-settings
new file mode 100644
index 0000000000000..fc22c90153950
--- /dev/null
+++ b/projects/packages/account-protection/changelog/add-protect-account-protection-settings
@@ -0,0 +1,4 @@
+Significance: minor
+Type: changed
+
+Moves get_settings method to primary class
diff --git a/projects/packages/account-protection/composer.json b/projects/packages/account-protection/composer.json
index b6a0271497be0..42431a12e7a10 100644
--- a/projects/packages/account-protection/composer.json
+++ b/projects/packages/account-protection/composer.json
@@ -4,7 +4,9 @@
"type": "jetpack-library",
"license": "GPL-2.0-or-later",
"require": {
- "php": ">=7.2"
+ "php": ">=7.2",
+ "automattic/jetpack-connection": "@dev",
+ "automattic/jetpack-status": "@dev"
},
"require-dev": {
"yoast/phpunit-polyfills": "^1.1.1",
diff --git a/projects/packages/account-protection/src/assets/jetpack-logo.svg b/projects/packages/account-protection/src/assets/jetpack-logo.svg
new file mode 100644
index 0000000000000..b91e3c5c216f5
--- /dev/null
+++ b/projects/packages/account-protection/src/assets/jetpack-logo.svg
@@ -0,0 +1,21 @@
+
diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php
index 0dd56070f3289..a63d54545889f 100644
--- a/projects/packages/account-protection/src/class-account-protection.php
+++ b/projects/packages/account-protection/src/class-account-protection.php
@@ -1,16 +1,205 @@
modules = $modules ?? new Modules();
+ $this->password_detection = $password_detection ?? new Password_Detection();
+ }
+
+ /**
+ * Initializes the configurations needed for the account protection module.
+ */
+ public function init(): void {
+ $this->register_hooks();
+
+ if ( $this->is_enabled() ) {
+ $this->register_runtime_hooks();
+ }
+ }
+
+ /**
+ * Register hooks for module activation and environment validation.
+ */
+ private function register_hooks(): void {
+ // Account protection activation/deactivation hooks
+ add_action( 'jetpack_activate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_activation' ) );
+ add_action( 'jetpack_deactivate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_deactivation' ) );
+
+ // Do not run in unsupported environments
+ add_action( 'jetpack_get_available_modules', array( $this, 'remove_module_on_unsupported_environments' ) );
+ add_action( 'jetpack_get_available_standalone_modules', array( $this, 'remove_standalone_module_on_unsupported_environments' ) );
+
+ // Register REST routes
+ add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) );
+ }
+
+ /**
+ * Register hooks for runtime operations.
+ */
+ private function register_runtime_hooks(): void {
+ // Validate password after successful login
+ add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 );
+
+ // Add password detection flow
+ add_action( 'login_form_password-detection', array( $this->password_detection, 'render_page' ), 10, 2 );
+
+ // Remove password detection usermeta after password reset and on profile password update
+ add_action( 'after_password_reset', array( $this->password_detection, 'delete_usermeta_after_password_reset' ), 10, 2 );
+ add_action( 'profile_update', array( $this->password_detection, 'delete_usermeta_on_profile_update' ), 10, 2 );
+
+ // Register AJAX resend password reset email action
+ add_action( 'wp_ajax_resend_password_reset', array( $this->password_detection, 'ajax_resend_password_reset_email' ) );
+ }
+
+ /**
+ * Activate the account protection on module activation.
+ */
+ public function on_account_protection_activation(): void {
+ // Activation logic can be added here
+ }
+
+ /**
+ * Deactivate the account protection on module deactivation.
+ */
+ public function on_account_protection_deactivation(): void {
+ // Remove password detection user meta on deactivation
+ // TODO: Run on Jetpack and Protect deactivation
+ $this->password_detection->delete_all_usermeta();
+ }
+
+ /**
+ * Determines if the account protection module is enabled on the site.
+ *
+ * @return bool
+ */
+ public function is_enabled() {
+ return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME );
+ }
+
+ /**
+ * Enables the account protection module.
+ *
+ * @return bool
+ */
+ public function enable() {
+ // Return true if already enabled.
+ if ( $this->is_enabled() ) {
+ return true;
+ }
+ return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false );
+ }
+
+ /**
+ * Disables the account protection module.
+ *
+ * @return bool
+ */
+ public function disable(): bool {
+ // Return true if already disabled.
+ if ( ! $this->is_enabled() ) {
+ return true;
+ }
+ return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME );
+ }
+
+ /**
+ * Determines if Account Protection is supported in the current environment.
+ *
+ * @return bool
+ */
+ public function is_supported_environment(): bool {
+ // Do not run when killswitch is enabled
+ if ( defined( 'DISABLE_JETPACK_ACCOUNT_PROTECTION' ) && DISABLE_JETPACK_ACCOUNT_PROTECTION ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Disables the Account Protection module when on an unsupported platform in Jetpack.
+ *
+ * @param array $modules Filterable value for `jetpack_get_available_modules`.
+ *
+ * @return array Array of module slugs.
+ */
+ public function remove_module_on_unsupported_environments( array $modules ): array {
+ if ( ! $this->is_supported_environment() ) {
+ // Account protection should never be available on unsupported platforms.
+ unset( $modules[ self::ACCOUNT_PROTECTION_MODULE_NAME ] );
+ }
+
+ return $modules;
+ }
+
+ /**
+ * Disables the Account Protection module when on an unsupported platform in a standalone plugin.
+ *
+ * @param array $modules Filterable value for `jetpack_get_available_standalone_modules`.
+ *
+ * @return array Array of module slugs.
+ */
+ public function remove_standalone_module_on_unsupported_environments( array $modules ): array {
+ if ( ! $this->is_supported_environment() ) {
+ // Account Protection should never be available on unsupported platforms.
+ $modules = array_filter(
+ $modules,
+ function ( $module ) {
+ return $module !== self::ACCOUNT_PROTECTION_MODULE_NAME;
+ }
+ );
+
+ }
+
+ return $modules;
+ }
+
+ /**
+ * Get the account protection settings.
+ *
+ * @return array
+ */
+ public function get_settings(): array {
+ $settings = array(
+ self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ),
+ );
- const PACKAGE_VERSION = '0.1.0-alpha';
+ return $settings;
+ }
}
diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php
new file mode 100644
index 0000000000000..ca195ed1207e6
--- /dev/null
+++ b/projects/packages/account-protection/src/class-password-detection.php
@@ -0,0 +1,388 @@
+password_reset_email = $password_reset_email ?? new Password_Reset_Email();
+ }
+
+ /**
+ * Redirect to the password detection page.
+ *
+ * @return string The URL to redirect to.
+ */
+ public function password_detection_redirect(): string {
+ return home_url( '/wp-login.php?action=password-detection' );
+ }
+
+ /**
+ * Check if the password is safe after login.
+ *
+ * @param \WP_User|\WP_Error $user The user or error object.
+ * @param string $password The password.
+ * @return \WP_User|\WP_Error The user object.
+ */
+ public function login_form_password_detection( $user, string $password ) {
+ // Check if the user is already a WP_Error object
+ if ( is_wp_error( $user ) ) {
+ return $user;
+ }
+
+ // Ensure the password is correct for this user
+ if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
+ return $user;
+ }
+
+ if ( ! $this->validate_password( $password ) ) {
+ // TODO: Ensure usermeta is always up to date
+ $this->update_usermeta( $user->ID, 'unsafe' );
+
+ // Redirect to the password detection page
+ add_filter( 'login_redirect', array( $this, 'password_detection_redirect' ), 10, 3 );
+ } else {
+ $this->update_usermeta( $user->ID, 'safe' );
+ }
+
+ return $user;
+ }
+
+ /**
+ * Render password detection page.
+ *
+ * @return never
+ */
+ public function render_page() {
+ // Restrict direct access to logged in users
+ $current_user = wp_get_current_user();
+ if ( 0 === $current_user->ID ) {
+ wp_safe_redirect( wp_login_url() );
+ exit;
+ }
+
+ // Restrict direct access to users with unsafe passwords
+ $user_password_status = $this->get_usermeta( $current_user->ID );
+ if ( ! $user_password_status || 'safe' === $user_password_status ) {
+ wp_safe_redirect( admin_url() );
+ exit;
+ }
+
+ // Use a transient to track email sent status
+ $transient_key = 'password_reset_email_sent_' . $current_user->ID;
+ $email_sent_flag = get_transient( $transient_key );
+
+ // Initialize template variables
+ $reset = false;
+ $context = 'Your current password was found in a public leak, which means your account might be at risk.';
+ $error = '';
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
+
+ // Handle reset_password_action form submission
+ if ( isset( $_POST['reset-password'] ) ) {
+ $reset = true;
+
+ // Verify nonce
+ if ( isset( $_POST['_wpnonce_reset_password'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_reset_password'] ) ), 'reset_password_action' ) ) {
+ // Send password reset email
+ if ( ! $email_sent_flag ) {
+ $email_sent = $this->password_reset_email->send();
+ if ( $email_sent ) {
+ // Set transient to mark the email as sent
+ set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS );
+ } else {
+ $error = 'email_send_error';
+ }
+ }
+
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_resend_password_reset_scripts' ) );
+ } else {
+ $error = 'reset_passowrd_nonce_verification_error';
+ }
+
+ // Handle proceed_action form submission
+ } elseif ( isset( $_POST['proceed'] ) ) {
+ $reset = true;
+
+ // Verify nonce
+ if ( isset( $_POST['_wpnonce_proceed'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_proceed'] ) ), 'proceed_action' ) ) {
+ wp_safe_redirect( admin_url() );
+ exit;
+ } else {
+ $error = 'proceed_nonce_verification_error';
+ }
+ }
+
+ $this->render_content( $reset, $context, $error, $this->password_reset_email->mask_email_address( $current_user->user_email ) );
+ exit;
+ }
+
+ /**
+ * Enqueue the resend password reset email scripts.
+ *
+ * @return void
+ */
+ public function enqueue_resend_password_reset_scripts(): void {
+ wp_enqueue_script( 'resend-password-reset', plugin_dir_url( __FILE__ ) . 'js/resend-password-reset.js', array( 'jquery' ), Account_Protection::PACKAGE_VERSION, true );
+
+ // Pass AJAX URL and nonce to the script
+ wp_localize_script(
+ 'resend-password-reset',
+ 'ajaxObject',
+ array(
+ 'ajax_url' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'resend_password_reset_nonce' ),
+ )
+ );
+ }
+
+ /**
+ * Enqueue the password detection page styles.
+ *
+ * @return void
+ */
+ public function enqueue_styles(): void {
+ wp_enqueue_style(
+ 'password-detection-styles',
+ plugin_dir_url( __FILE__ ) . 'css/password-detection.css',
+ array(),
+ Account_Protection::PACKAGE_VERSION
+ );
+ }
+
+ /**
+ * Run AJAX request to resend password reset email.
+ */
+ public function ajax_resend_password_reset_email() {
+ // Verify the nonce for security
+ check_ajax_referer( 'resend_password_reset_nonce', 'security' );
+
+ // Check if the user is logged in
+ if ( ! is_user_logged_in() ) {
+ wp_send_json_error( array( 'message' => 'User not authenticated' ) );
+ }
+
+ // Resend the email
+ $email_sent = $this->password_reset_email->send();
+ if ( $email_sent ) {
+ wp_send_json_success( array( 'message' => 'Resend successful.' ) );
+ } else {
+ wp_send_json_error( array( 'message' => 'Resend failed. ' ) );
+ }
+ }
+
+ /**
+ * Password validation.
+ *
+ * @param string $password The password to validate.
+ * @return bool True if the password is valid, false otherwise.
+ */
+ public function validate_password( string $password ): bool {
+ // TODO: Uncomment out once endpoint is live
+ // Check compromised and common passwords
+ // $weak_password = self::check_weak_passwords( $password );
+
+ return $password ? false : true;
+ }
+
+ /**
+ * Check if the password is in the list of common/compromised passwords.
+ *
+ * @param string $password The password to check.
+ * @return bool|\WP_Error True if the password is in the list of common/compromised passwords, false otherwise.
+ */
+ public function check_weak_passwords( string $password ) {
+ $api_url = '/jetpack-protect-weak-password';
+
+ $is_connected = ( new Connection_Manager() )->is_connected();
+
+ if ( ! $is_connected ) {
+ return new \WP_Error( 'site_not_connected' );
+ }
+
+ // Hash pass with sha1, and pass first 5 characters to the API
+ $hashed_password = sha1( $password );
+ $password_prefix = substr( $hashed_password, 0, 5 );
+
+ $response = Client::wpcom_json_api_request_as_blog(
+ $api_url . '/' . $password_prefix,
+ '2',
+ array( 'method' => 'GET' ),
+ null,
+ 'wpcom'
+ );
+
+ $response_code = wp_remote_retrieve_response_code( $response );
+
+ if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) {
+ return new \WP_Error( 'failed_fetching_weak_passwords', 'Failed to fetch weak passwords from the server', array( 'status' => $response_code ) );
+ }
+
+ $body = json_decode( wp_remote_retrieve_body( $response ), true );
+
+ // Check if the password is in the list of common/compromised passwords
+ $password_suffix = substr( $hashed_password, 5 );
+ if ( in_array( $password_suffix, $body['compromised'] ?? array(), true ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the password detection usermeta.
+ *
+ * @param int $user_id The user ID.
+ */
+ public function get_usermeta( int $user_id ) {
+ return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true );
+ }
+
+ /**
+ * Update the password detection usermeta.
+ *
+ * @param int $user_id The user ID.
+ * @param string $setting The password detection setting.
+ */
+ public function update_usermeta( int $user_id, string $setting ) {
+ update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting );
+ }
+
+ /**
+ * Delete password detection usermeta for all users.
+ */
+ public function delete_all_usermeta() {
+ $users = get_users();
+ foreach ( $users as $user ) {
+ $this->delete_usermeta( $user->ID );
+ }
+ }
+
+ /**
+ * Delete the password detection usermeta.
+ *
+ * @param int $user_id The user ID.
+ */
+ public function delete_usermeta( int $user_id ) {
+ delete_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY );
+ }
+
+ /**
+ * Delete the password detection usermeta after password reset.
+ *
+ * @param \WP_User $user The user object.
+ */
+ public function delete_usermeta_after_password_reset( \WP_User $user ) {
+ $this->delete_usermeta( $user->ID );
+ }
+
+ /**
+ * Delete the password detection usermeta on profile password update.
+ *
+ * @param int $user_id The user ID.
+ */
+ public function delete_usermeta_on_profile_update( int $user_id ) {
+ if (
+ ! empty( $_POST['_wpnonce'] ) &&
+ wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id )
+ ) {
+ if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) {
+ $this->delete_usermeta( $user_id );
+ }
+ }
+ }
+
+ /**
+ * Render content for password detection page.
+ *
+ * @param bool $reset Whether the user is resetting their password.
+ * @param string $context The context for the password detection page.
+ * @param string $error The error message to display.
+ * @param string $masked_email The masked email address.
+ * @return void
+ */
+ public function render_content( bool $reset, string $context, string $error, string $masked_email ): void {
+ defined( 'ABSPATH' ) || exit;
+ ?>
+
+
+
+
@@ -112,6 +115,7 @@ export class Security extends Component {
>
) }
+ { foundAccountProtection &&
}
{ foundWaf &&
}
{ foundProtect &&
}
{ ( foundWaf || foundProtect ) &&
}
diff --git a/projects/plugins/jetpack/_inc/client/security/style.scss b/projects/plugins/jetpack/_inc/client/security/style.scss
index 9a4608ee3bf57..385e7feaa710f 100644
--- a/projects/plugins/jetpack/_inc/client/security/style.scss
+++ b/projects/plugins/jetpack/_inc/client/security/style.scss
@@ -56,7 +56,9 @@
}
&__share-data-popover {
- margin-left: 8px;
+ display: flex;
+ align-items: center;
+ margin-left: 4px;
}
&__upgrade-popover {
@@ -189,4 +191,24 @@
.jp-form-settings-group p {
margin-bottom: 0.5rem;
+}
+
+.account-protection__settings {
+ &__toggle-setting {
+ flex-wrap: wrap;
+ display: flex;
+ margin-bottom: 24px;
+
+ &__label {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ &__strict-mode-popover {
+ display: flex;
+ align-items: center;
+ margin-left: 4px;
+ }
+
}
\ No newline at end of file
diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js b/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js
new file mode 100644
index 0000000000000..feee531d78a38
--- /dev/null
+++ b/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js
@@ -0,0 +1,66 @@
+import restApi from '@automattic/jetpack-api';
+import {
+ ACCOUNT_PROTECTION_SETTINGS_FETCH,
+ ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE,
+ ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL,
+ ACCOUNT_PROTECTION_SETTINGS_UPDATE,
+ ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS,
+ ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL,
+} from 'state/action-types';
+
+export const fetchAccountProtectionSettings = () => {
+ return dispatch => {
+ dispatch( {
+ type: ACCOUNT_PROTECTION_SETTINGS_FETCH,
+ } );
+ return restApi
+ .fetchAccountProtectionSettings()
+ .then( settings => {
+ dispatch( {
+ type: ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE,
+ settings,
+ } );
+ return settings;
+ } )
+ .catch( error => {
+ dispatch( {
+ type: ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL,
+ error: error,
+ } );
+ } );
+ };
+};
+
+/**
+ * Update Account Protection Settings
+ *
+ * @param {object} newSettings - The new settings to be saved.
+ * @param {boolean} newSettings.strictMode - Whether strict mode is enabled.
+ * @return {Function} - The action.
+ */
+export const updateAccountProtectionSettings = newSettings => {
+ return dispatch => {
+ dispatch( {
+ type: ACCOUNT_PROTECTION_SETTINGS_UPDATE,
+ } );
+ return restApi
+ .updateAccountProtectionSettings( {
+ jetpack_account_protection_strict_mode: newSettings.strictMode,
+ } )
+ .then( settings => {
+ dispatch( {
+ type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS,
+ settings,
+ } );
+ return settings;
+ } )
+ .catch( error => {
+ dispatch( {
+ type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL,
+ error: error,
+ } );
+
+ throw error;
+ } );
+ };
+};
diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/index.js b/projects/plugins/jetpack/_inc/client/state/account-protection/index.js
new file mode 100644
index 0000000000000..5e3164b4c9f72
--- /dev/null
+++ b/projects/plugins/jetpack/_inc/client/state/account-protection/index.js
@@ -0,0 +1,2 @@
+export * from './reducer';
+export * from './actions';
diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js b/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js
new file mode 100644
index 0000000000000..cb42d7bccc486
--- /dev/null
+++ b/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js
@@ -0,0 +1,87 @@
+import { assign, get } from 'lodash';
+import { combineReducers } from 'redux';
+import {
+ ACCOUNT_PROTECTION_SETTINGS_FETCH,
+ ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE,
+ ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL,
+ ACCOUNT_PROTECTION_SETTINGS_UPDATE,
+ ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS,
+ ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL,
+} from 'state/action-types';
+
+export const data = ( state = {}, action ) => {
+ switch ( action.type ) {
+ case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE:
+ case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS:
+ return assign( {}, state, {
+ strictMode: Boolean( action.settings?.jetpack_account_protection_strict_mode ),
+ } );
+ default:
+ return state;
+ }
+};
+
+export const initialRequestsState = {
+ isFetchingAccountProtectionSettings: false,
+ isUpdatingAccountProtectionSettings: false,
+};
+
+export const requests = ( state = initialRequestsState, action ) => {
+ switch ( action.type ) {
+ case ACCOUNT_PROTECTION_SETTINGS_FETCH:
+ return assign( {}, state, {
+ isFetchingAccountProtectionSettings: true,
+ } );
+ case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE:
+ case ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL:
+ return assign( {}, state, {
+ isFetchingAccountProtectionSettings: false,
+ } );
+ case ACCOUNT_PROTECTION_SETTINGS_UPDATE:
+ return assign( {}, state, {
+ isUpdatingAccountProtectionSettings: true,
+ } );
+ case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS:
+ case ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL:
+ return assign( {}, state, {
+ isUpdatingAccountProtectionSettings: false,
+ } );
+ default:
+ return state;
+ }
+};
+
+export const reducer = combineReducers( {
+ data,
+ requests,
+} );
+
+/**
+ * Returns true if currently requesting the account protection settings. Otherwise false.
+ *
+ * @param {object} state - Global state tree
+ * @return {boolean} Whether the account protection settings are being requested
+ */
+export function isFetchingAccountProtectionSettings( state ) {
+ return !! state.jetpack.accountProtection.requests.isFetchingAccountProtectionSettings;
+}
+
+/**
+ * Returns true if currently updating the account protection settings. Otherwise false.
+ *
+ * @param {object} state - Global state tree
+ * @return {boolean} Whether the account protection settings are being requested
+ */
+export function isUpdatingAccountProtectionSettings( state ) {
+ return !! state.jetpack.accountProtection.requests.isUpdatingAccountProtectionSettings;
+}
+
+/**
+ * Returns the account protection's settings.
+ *
+ * @param {object} state - Global state tree
+ * @return {string} File path to bootstrap.php
+ */
+export function getAccountProtectionSettings( state ) {
+ return get( state.jetpack.accountProtection, [ 'data' ], {} );
+}
diff --git a/projects/plugins/jetpack/_inc/client/state/action-types.js b/projects/plugins/jetpack/_inc/client/state/action-types.js
index c4785d4a2ced5..7da1fbb07cf1d 100644
--- a/projects/plugins/jetpack/_inc/client/state/action-types.js
+++ b/projects/plugins/jetpack/_inc/client/state/action-types.js
@@ -245,6 +245,15 @@ export const JETPACK_LICENSING_GET_USER_LICENSES_FAILURE =
export const JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL =
'JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL';
+export const ACCOUNT_PROTECTION_SETTINGS_FETCH = 'ACCOUNT_PROTECTION_SETTINGS_FETCH';
+export const ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE =
+ 'ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE';
+export const ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL';
+export const ACCOUNT_PROTECTION_SETTINGS_UPDATE = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE';
+export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS =
+ 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS';
+export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL';
+
export const WAF_SETTINGS_FETCH = 'WAF_SETTINGS_FETCH';
export const WAF_SETTINGS_FETCH_RECEIVE = 'WAF_SETTINGS_FETCH_RECEIVE';
export const WAF_SETTINGS_FETCH_FAIL = 'WAF_SETTINGS_FETCH_FAIL';
diff --git a/projects/plugins/jetpack/_inc/client/state/reducer.js b/projects/plugins/jetpack/_inc/client/state/reducer.js
index 5ff156b807a49..14e85f0fb5289 100644
--- a/projects/plugins/jetpack/_inc/client/state/reducer.js
+++ b/projects/plugins/jetpack/_inc/client/state/reducer.js
@@ -1,5 +1,6 @@
import { combineReducers } from 'redux';
import { globalNotices } from 'components/global-notices/state/notices/reducer';
+import { reducer as accountProtection } from 'state/account-protection/reducer';
import { dashboard } from 'state/at-a-glance/reducer';
import { reducer as connection } from 'state/connection/reducer';
import { reducer as devCard } from 'state/dev-version/reducer';
@@ -46,6 +47,7 @@ const jetpackReducer = combineReducers( {
disconnectSurvey,
trackingSettings,
licensing,
+ accountProtection,
waf,
introOffers,
} );
diff --git a/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php b/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php
index 500069fb7ca1e..06cf25d1268f7 100644
--- a/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php
+++ b/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php
@@ -2358,7 +2358,14 @@ public static function get_updateable_data_list( $selector = '' ) {
'validate_callback' => __CLASS__ . '::validate_posint',
'jp_group' => 'custom-content-types',
),
-
+ // Account Protection.
+ 'jetpack_account_protection_strict_mode' => array(
+ 'description' => esc_html__( 'Strict mode - Require strong passwords.', 'jetpack' ),
+ 'type' => 'boolean',
+ 'default' => 0,
+ 'validate_callback' => __CLASS__ . '::validate_boolean',
+ 'jp_group' => 'account-protection',
+ ),
// WAF.
'jetpack_waf_automatic_rules' => array(
'description' => esc_html__( 'Enable automatic rules - Protect your site against untrusted traffic sources with automatic security rules.', 'jetpack' ),
diff --git a/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings b/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings
new file mode 100644
index 0000000000000..4c36bca9e49ec
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Adds the Account Protection module toggle
diff --git a/projects/plugins/jetpack/composer.json b/projects/plugins/jetpack/composer.json
index 7092b747b3737..84db645d3aef0 100644
--- a/projects/plugins/jetpack/composer.json
+++ b/projects/plugins/jetpack/composer.json
@@ -12,6 +12,7 @@
"ext-json": "*",
"ext-openssl": "*",
"automattic/jetpack-a8c-mc-stats": "@dev",
+ "automattic/jetpack-account-protection": "@dev",
"automattic/jetpack-admin-ui": "@dev",
"automattic/jetpack-assets": "@dev",
"automattic/jetpack-autoloader": "@dev",
diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock
index 2c210154d384a..2118c86f2f421 100644
--- a/projects/plugins/jetpack/composer.lock
+++ b/projects/plugins/jetpack/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "7b6c53f88fcb9c7098d80137fd6d13c1",
+ "content-hash": "cbb88a4e4e1b0088ff12393af82e5cdc",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
@@ -59,6 +59,78 @@
"relative": true
}
},
+ {
+ "name": "automattic/jetpack-account-protection",
+ "version": "dev-trunk",
+ "dist": {
+ "type": "path",
+ "url": "../../packages/account-protection",
+ "reference": "badc1036552f26a900a69608df22284e603981ed"
+ },
+ "require": {
+ "automattic/jetpack-connection": "@dev",
+ "automattic/jetpack-status": "@dev",
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "automattic/jetpack-changelogger": "@dev",
+ "automattic/wordbless": "dev-master",
+ "yoast/phpunit-polyfills": "^1.1.1"
+ },
+ "suggest": {
+ "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
+ },
+ "type": "jetpack-library",
+ "extra": {
+ "autotagger": true,
+ "branch-alias": {
+ "dev-trunk": "0.1.x-dev"
+ },
+ "changelogger": {
+ "link-template": "https://github.com/Automattic/jetpack-account-protection/compare/v${old}...v${new}"
+ },
+ "mirror-repo": "Automattic/jetpack-account-protection",
+ "textdomain": "jetpack-account-protection",
+ "version-constants": {
+ "::PACKAGE_VERSION": "src/class-account-protection.php"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "scripts": {
+ "build-development": [
+ "echo 'Add your build step to composer.json, please!'"
+ ],
+ "build-production": [
+ "echo 'Add your build step to composer.json, please!'"
+ ],
+ "phpunit": [
+ "./vendor/phpunit/phpunit/phpunit --colors=always"
+ ],
+ "post-install-cmd": [
+ "WorDBless\\Composer\\InstallDropin::copy"
+ ],
+ "post-update-cmd": [
+ "WorDBless\\Composer\\InstallDropin::copy"
+ ],
+ "test-coverage": [
+ "php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php \"$COVERAGE_DIR/php.cov\""
+ ],
+ "test-php": [
+ "@composer phpunit"
+ ]
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "Account protection",
+ "transport-options": {
+ "relative": true
+ }
+ },
{
"name": "automattic/jetpack-admin-ui",
"version": "dev-trunk",
@@ -6084,6 +6156,7 @@
"minimum-stability": "dev",
"stability-flags": {
"automattic/jetpack-a8c-mc-stats": 20,
+ "automattic/jetpack-account-protection": 20,
"automattic/jetpack-admin-ui": 20,
"automattic/jetpack-assets": 20,
"automattic/jetpack-autoloader": 20,
diff --git a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php
index 9852478a7c53d..4b931dadb330f 100644
--- a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php
+++ b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php
@@ -127,6 +127,7 @@
'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled',
'jetpack_subscriptions_subscribe_navigation_enabled' => '(Bool) Whether the Subscribe block navigation placement is enabled',
'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt',
+ 'jetpack_account_protection_strict_mode' => '(bool) Whether to enforce strict password requirements',
'jetpack_waf_automatic_rules' => '(bool) Whether the WAF should enforce automatic firewall rules',
'jetpack_waf_ip_allow_list' => '(string) List of IP addresses to always allow',
'jetpack_waf_ip_allow_list_enabled' => '(bool) Whether the IP allow list is enabled',
@@ -490,6 +491,7 @@ function ( $newsletter_category ) {
'jetpack_comment_form_color_scheme' => (string) get_option( 'jetpack_comment_form_color_scheme' ),
'in_site_migration_flow' => (string) get_option( 'in_site_migration_flow', '' ),
'migration_source_site_domain' => (string) get_option( 'migration_source_site_domain' ),
+ 'jetpack_account_protection_strict_mode' => (bool) get_option( 'jetpack_account_protection_strict_mode' ),
'jetpack_waf_automatic_rules' => (bool) get_option( 'jetpack_waf_automatic_rules' ),
'jetpack_waf_ip_allow_list' => (string) get_option( 'jetpack_waf_ip_allow_list' ),
'jetpack_waf_ip_allow_list_enabled' => (bool) get_option( 'jetpack_waf_ip_allow_list_enabled' ),
diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php
new file mode 100644
index 0000000000000..c552efec4cc41
--- /dev/null
+++ b/projects/plugins/jetpack/modules/account-protection.php
@@ -0,0 +1,18 @@
+init();
diff --git a/projects/plugins/jetpack/modules/waf.php b/projects/plugins/jetpack/modules/waf.php
index 1d5a5984f4bab..0df3856fb1948 100644
--- a/projects/plugins/jetpack/modules/waf.php
+++ b/projects/plugins/jetpack/modules/waf.php
@@ -1,7 +1,7 @@
=7.2"
+ },
+ "require-dev": {
+ "automattic/jetpack-changelogger": "@dev",
+ "automattic/wordbless": "dev-master",
+ "yoast/phpunit-polyfills": "^1.1.1"
+ },
+ "suggest": {
+ "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
+ },
+ "type": "jetpack-library",
+ "extra": {
+ "autotagger": true,
+ "branch-alias": {
+ "dev-trunk": "0.1.x-dev"
+ },
+ "changelogger": {
+ "link-template": "https://github.com/Automattic/jetpack-account-protection/compare/v${old}...v${new}"
+ },
+ "mirror-repo": "Automattic/jetpack-account-protection",
+ "textdomain": "jetpack-account-protection",
+ "version-constants": {
+ "::PACKAGE_VERSION": "src/class-account-protection.php"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "scripts": {
+ "build-development": [
+ "echo 'Add your build step to composer.json, please!'"
+ ],
+ "build-production": [
+ "echo 'Add your build step to composer.json, please!'"
+ ],
+ "phpunit": [
+ "./vendor/phpunit/phpunit/phpunit --colors=always"
+ ],
+ "post-install-cmd": [
+ "WorDBless\\Composer\\InstallDropin::copy"
+ ],
+ "post-update-cmd": [
+ "WorDBless\\Composer\\InstallDropin::copy"
+ ],
+ "test-coverage": [
+ "php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php \"$COVERAGE_DIR/php.cov\""
+ ],
+ "test-php": [
+ "@composer phpunit"
+ ]
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "Account protection",
+ "transport-options": {
+ "relative": true
+ }
+ },
{
"name": "automattic/jetpack-admin-ui",
"version": "dev-trunk",
@@ -4628,6 +4700,7 @@
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
+ "automattic/jetpack-account-protection": 20,
"automattic/jetpack-admin-ui": 20,
"automattic/jetpack-assets": 20,
"automattic/jetpack-autoloader": 20,
diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php
index 660045d426534..ea02244a44e5a 100644
--- a/projects/plugins/protect/src/class-jetpack-protect.php
+++ b/projects/plugins/protect/src/class-jetpack-protect.php
@@ -9,6 +9,7 @@
exit( 0 );
}
+use Automattic\Jetpack\Account_Protection\Account_Protection;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
@@ -58,6 +59,7 @@ class Jetpack_Protect {
);
const JETPACK_WAF_MODULE_SLUG = 'waf';
const JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG = 'protect';
+ const JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG = 'account-protection';
const JETPACK_PROTECT_ACTIVATION_OPTION = JETPACK_PROTECT_SLUG . '_activated';
/**
@@ -112,6 +114,9 @@ function () {
// Web application firewall package.
$config->ensure( 'waf' );
+
+ // Account protection package.
+ $config->ensure( 'account_protection' );
},
1
);
@@ -133,6 +138,7 @@ public function init() {
REST_Controller::init();
My_Jetpack_Initializer::init();
Site_Health::init();
+ ( new Account_Protection() )->init();
// Sets up JITMS.
JITM::configure();
@@ -208,6 +214,7 @@ public function initial_state() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$refresh_status_from_wpcom = isset( $_GET['checkPlan'] );
$status = Status::get_status( $refresh_status_from_wpcom );
+ $account_protection = new Account_Protection();
$initial_state = array(
'apiRoot' => esc_url_raw( rest_url() ),
@@ -226,6 +233,10 @@ public function initial_state() {
'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ),
'hasPlan' => Plan::has_required_plan(),
'onboardingProgress' => Onboarding::get_current_user_progress(),
+ 'accountProtection' => array(
+ 'isEnabled' => $account_protection->is_enabled(),
+ 'settings' => $account_protection->get_settings(),
+ ),
'waf' => array(
'wafSupported' => Waf_Runner::is_supported_environment(),
'currentIp' => IP_Utils::get_ip(),
@@ -281,6 +292,7 @@ public static function activate_modules() {
delete_option( self::JETPACK_PROTECT_ACTIVATION_OPTION );
( new Modules() )->activate( self::JETPACK_WAF_MODULE_SLUG, false, false );
( new Modules() )->activate( self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, false, false );
+ ( new Modules() )->activate( self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG, false, false );
}
/**
@@ -338,7 +350,7 @@ public function admin_bar( $wp_admin_bar ) {
* @return array
*/
public function protect_filter_available_modules( $modules ) {
- return array_merge( array( self::JETPACK_WAF_MODULE_SLUG, self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG ), $modules );
+ return array_merge( array( self::JETPACK_WAF_MODULE_SLUG, self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG ), $modules );
}
/**
diff --git a/projects/plugins/protect/src/class-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php
index 0aa752ddfd6d1..b6ddb432afa23 100644
--- a/projects/plugins/protect/src/class-rest-controller.php
+++ b/projects/plugins/protect/src/class-rest-controller.php
@@ -9,6 +9,7 @@
namespace Automattic\Jetpack\Protect;
+use Automattic\Jetpack\Account_Protection\Account_Protection;
use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
use Automattic\Jetpack\IP\Utils as IP_Utils;
use Automattic\Jetpack\Protect_Status\REST_Controller as Protect_Status_REST_Controller;
@@ -117,6 +118,30 @@ public static function register_rest_endpoints() {
)
);
+ register_rest_route(
+ 'jetpack-protect/v1',
+ 'toggle-account-protection',
+ array(
+ 'methods' => \WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::api_toggle_account_protection',
+ 'permission_callback' => function () {
+ return current_user_can( 'manage_options' );
+ },
+ )
+ );
+
+ register_rest_route(
+ 'jetpack-protect/v1',
+ 'account-protection',
+ array(
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => __CLASS__ . '::api_get_account_protection',
+ 'permission_callback' => function () {
+ return current_user_can( 'manage_options' );
+ },
+ )
+ );
+
register_rest_route(
'jetpack-protect/v1',
'toggle-waf',
@@ -340,6 +365,47 @@ public static function api_scan() {
return new WP_REST_Response( 'Scan enqueued.' );
}
+ /**
+ * Toggles the Account Protection module on or off for the API endpoint
+ *
+ * @return WP_REST_Response|WP_Error
+ */
+ public static function api_toggle_account_protection() {
+ $account_protection = new Account_Protection();
+ if ( $account_protection->is_enabled() ) {
+ $disabled = $account_protection->disable();
+ if ( ! $disabled ) {
+ return new WP_Error(
+ 'account_protection_disable_failed',
+ __( 'An error occurred disabling account protection.', 'jetpack-protect' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ return rest_ensure_response( true );
+ }
+
+ $enabled = $account_protection->enable();
+ if ( ! $enabled ) {
+ return new WP_Error(
+ 'account_protection_enable_failed',
+ __( 'An error occurred enabling account protection.', 'jetpack-protect' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ return rest_ensure_response( true );
+ }
+
+ /**
+ * Get Account Protection data for the API endpoint
+ *
+ * @return WP_Rest_Response
+ */
+ public static function api_get_account_protection() {
+ return new WP_REST_Response( ( new Account_Protection() )->get_settings() );
+ }
+
/**
* Toggles the WAF module on or off for the API endpoint
*
diff --git a/projects/plugins/protect/src/js/api.ts b/projects/plugins/protect/src/js/api.ts
index 2b98a6164bf8b..186ac89c2c513 100644
--- a/projects/plugins/protect/src/js/api.ts
+++ b/projects/plugins/protect/src/js/api.ts
@@ -1,8 +1,29 @@
-import { type FixersStatus, type ScanStatus, type WafStatus } from '@automattic/jetpack-scan';
+import { type FixersStatus, type ScanStatus } from '@automattic/jetpack-scan';
import apiFetch from '@wordpress/api-fetch';
import camelize from 'camelize';
+import { AccountProtectionStatus } from './types/account-protection';
+import { WafStatus } from './types/waf';
const API = {
+ getAccountProtection: (): Promise< AccountProtectionStatus > =>
+ apiFetch( {
+ path: 'jetpack-protect/v1/account-protection',
+ method: 'GET',
+ } ),
+
+ toggleAccountProtection: () =>
+ apiFetch( {
+ method: 'POST',
+ path: 'jetpack-protect/v1/toggle-account-protection',
+ } ),
+
+ updateAccountProtection: data =>
+ apiFetch( {
+ method: 'POST',
+ path: 'jetpack/v4/account-protection',
+ data,
+ } ).then( camelize ),
+
getWaf: (): Promise< WafStatus > =>
apiFetch( {
path: 'jetpack-protect/v1/waf',
diff --git a/projects/plugins/protect/src/js/components/admin-page/index.jsx b/projects/plugins/protect/src/js/components/admin-page/index.jsx
index 4579831b5f0a5..95e34eb79daa8 100644
--- a/projects/plugins/protect/src/js/components/admin-page/index.jsx
+++ b/projects/plugins/protect/src/js/components/admin-page/index.jsx
@@ -69,6 +69,10 @@ const AdminPage = ( { children } ) => {
>
}
/>
+
{ __( 'Settings', 'jetpack-protect' ) } }
+ />
{ children }
diff --git a/projects/plugins/protect/src/js/constants.js b/projects/plugins/protect/src/js/constants.js
index 5ec94bdccafc9..f643493fd37bd 100644
--- a/projects/plugins/protect/src/js/constants.js
+++ b/projects/plugins/protect/src/js/constants.js
@@ -31,3 +31,4 @@ export const QUERY_ONBOARDING_PROGRESS_KEY = 'onboarding progress';
export const QUERY_PRODUCT_DATA_KEY = 'product data';
export const QUERY_SCAN_STATUS_KEY = 'scan status';
export const QUERY_WAF_KEY = 'waf';
+export const QUERY_ACCOUNT_PROTECTION_KEY = 'account protection';
diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts
new file mode 100644
index 0000000000000..592c5b983c37a
--- /dev/null
+++ b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts
@@ -0,0 +1,57 @@
+import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query';
+import { __ } from '@wordpress/i18n';
+import camelize from 'camelize';
+import API from '../../api';
+import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants';
+import useNotices from '../../hooks/use-notices';
+import { AccountProtectionStatus } from '../../types/account-protection';
+
+/**
+ * Account Protection Mutatation Hook
+ *
+ * @return {UseMutationResult} useMutation result.
+ */
+export default function useAccountProtectionMutation(): UseMutationResult<
+ unknown,
+ { [ key: string ]: unknown },
+ unknown,
+ { initialValue: AccountProtectionStatus }
+> {
+ const queryClient = useQueryClient();
+ const { showSuccessNotice, showSavingNotice, showErrorNotice } = useNotices();
+
+ return useMutation( {
+ mutationFn: API.updateAccountProtection,
+ onMutate: settings => {
+ showSavingNotice();
+
+ // Get the current Account Protection settings.
+ const initialValue = queryClient.getQueryData( [
+ QUERY_ACCOUNT_PROTECTION_KEY,
+ ] ) as AccountProtectionStatus;
+
+ // Optimistically update the Account Protection settings.
+ queryClient.setQueryData(
+ [ QUERY_ACCOUNT_PROTECTION_KEY ],
+ ( accountProtectionStatus: AccountProtectionStatus ) => ( {
+ ...accountProtectionStatus,
+ settings: {
+ ...accountProtectionStatus.settings,
+ ...camelize( settings ),
+ },
+ } )
+ );
+
+ return { initialValue };
+ },
+ onSuccess: () => {
+ showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) );
+ },
+ onError: ( error, variables, context ) => {
+ // Reset the account protection config to its previous state.
+ queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], context.initialValue );
+
+ showErrorNotice( __( 'Error saving changes.', 'jetpack-protect' ) );
+ },
+ } );
+}
diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts
new file mode 100644
index 0000000000000..01dd3354432a9
--- /dev/null
+++ b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts
@@ -0,0 +1,18 @@
+import { useQuery, UseQueryResult } from '@tanstack/react-query';
+import camelize from 'camelize';
+import API from '../../api';
+import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants';
+import { AccountProtectionStatus } from '../../types/account-protection';
+
+/**
+ * Account Protection Query Hook
+ *
+ * @return {UseQueryResult} useQuery result.
+ */
+export default function useAccountProtectionQuery(): UseQueryResult< AccountProtectionStatus > {
+ return useQuery( {
+ queryKey: [ QUERY_ACCOUNT_PROTECTION_KEY ],
+ queryFn: API.getAccountProtection,
+ initialData: camelize( window?.jetpackProtectInitialState?.accountProtection ),
+ } );
+}
diff --git a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts
new file mode 100644
index 0000000000000..2f8ca342902ea
--- /dev/null
+++ b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts
@@ -0,0 +1,45 @@
+import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query';
+import { __ } from '@wordpress/i18n';
+import API from '../../api';
+import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants';
+import useNotices from '../../hooks/use-notices';
+import { AccountProtectionStatus } from '../../types/account-protection';
+
+/**
+ * Toggle Account Protection Mutatation
+ *
+ * @return {UseMutationResult} useMutation result.
+ */
+export default function useToggleAccountProtectionMutation(): UseMutationResult {
+ const queryClient = useQueryClient();
+ const { showSavingNotice, showSuccessNotice, showErrorNotice } = useNotices();
+
+ return useMutation( {
+ mutationFn: API.toggleAccountProtection,
+ onMutate: () => {
+ showSavingNotice();
+
+ // Get the current Account Protection settings.
+ const initialValue = queryClient.getQueryData( [
+ QUERY_ACCOUNT_PROTECTION_KEY,
+ ] ) as AccountProtectionStatus;
+
+ // Optimistically update the Account Protection settings.
+ queryClient.setQueryData(
+ [ QUERY_ACCOUNT_PROTECTION_KEY ],
+ ( accountProtectionStatus: AccountProtectionStatus ) => ( {
+ ...accountProtectionStatus,
+ isEnabled: ! initialValue.isEnabled,
+ } )
+ );
+
+ return { initialValue };
+ },
+ onSuccess: () => {
+ showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) );
+ },
+ onError: () => {
+ showErrorNotice( __( 'Error savings changes.', 'jetpack-protect' ) );
+ },
+ } );
+}
diff --git a/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx b/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx
new file mode 100644
index 0000000000000..90e473c270bc6
--- /dev/null
+++ b/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx
@@ -0,0 +1,59 @@
+import { useCallback } from 'react';
+import useAccountProtectionMutation from '../../data/account-protection/use-account-protection-mutation';
+import useAccountProtectionQuery from '../../data/account-protection/use-account-protection-query';
+import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-module-mutation';
+import useAnalyticsTracks from '../use-analytics-tracks';
+
+/**
+ * Use Account Protection Data Hook
+ *
+ * @return {object} Account Protection data and methods for interacting with it.
+ */
+const useAccountProtectionData = () => {
+ const { recordEvent } = useAnalyticsTracks();
+ const { data: accountProtection } = useAccountProtectionQuery();
+ const accountProtectionMutation = useAccountProtectionMutation();
+ const toggleAccountProtectionMutation = useToggleAccountProtectionMutation();
+
+ /**
+ * Toggle Account Protection Module
+ *
+ * Flips the switch on the Account Protection module, and then refreshes the data.
+ */
+ const toggleAccountProtection = useCallback( async () => {
+ toggleAccountProtectionMutation.mutate();
+ }, [ toggleAccountProtectionMutation ] );
+
+ /**
+ * Toggle Strict Mode
+ *
+ * Flips the switch on the strict mode option, and then refreshes the data.
+ */
+ const toggleStrictMode = useCallback( async () => {
+ const value = ! accountProtection.settings.jetpackAccountProtectionStrictMode;
+ const mutationObj = { jetpack_account_protection_strict_mode: value };
+ if ( ! value ) {
+ mutationObj.jetpack_account_protection_strict_mode = false;
+ }
+ await accountProtectionMutation.mutateAsync( mutationObj );
+ recordEvent(
+ mutationObj
+ ? 'jetpack_account_protection_strict_mode_enabled'
+ : 'jetpack_account_protection_strict_mode_disabled'
+ );
+ }, [
+ recordEvent,
+ accountProtection.settings.jetpackAccountProtectionStrictMode,
+ accountProtectionMutation,
+ ] );
+
+ return {
+ ...accountProtection,
+ isUpdating: accountProtectionMutation.isPending,
+ isToggling: toggleAccountProtectionMutation.isPending,
+ toggleAccountProtection,
+ toggleStrictMode,
+ };
+};
+
+export default useAccountProtectionData;
diff --git a/projects/plugins/protect/src/js/index.tsx b/projects/plugins/protect/src/js/index.tsx
index b8983d65bb836..3ffe20e853986 100644
--- a/projects/plugins/protect/src/js/index.tsx
+++ b/projects/plugins/protect/src/js/index.tsx
@@ -13,6 +13,7 @@ import { CheckoutProvider } from './hooks/use-plan';
import FirewallRoute from './routes/firewall';
import ScanRoute from './routes/scan';
import ScanHistoryRoute from './routes/scan/history';
+import SettingsRoute from './routes/settings';
import SetupRoute from './routes/setup';
import './styles.module.scss';
@@ -56,6 +57,7 @@ function render() {
+ } />
} />
} />
{
+ const { hasPlan } = usePlan();
+ const {
+ settings: { jetpackAccountProtectionStrictMode: strictMode },
+ isEnabled: isAccountProtectionEnabled,
+ toggleAccountProtection,
+ toggleStrictMode,
+ isToggling,
+ isUpdating,
+ } = useAccountProtectionData();
+
+ // Track view for Protect Account Protection page.
+ useAnalyticsTracks( {
+ pageViewEventName: 'protect_account_protection',
+ pageViewEventProperties: {
+ has_plan: hasPlan,
+ },
+ } );
+
+ const accountProtectionSettings = (
+
+
+
+
+
+
+ { __( 'Account protection', 'jetpack-protect' ) }
+
+
+ { createInterpolateElement(
+ __(
+ 'When enabled, users can only set passwords that meet strong security standards, helping protect their accounts and your site.',
+ 'jetpack-protect'
+ ),
+ {
+ link: , // TODO: Update this redirect URL
+ }
+ ) }
+
+
+
+ );
+
+ const strictModeSettings = (
+
+
+
+
+
+
+ { __( 'Require strongs passwords', 'jetpack-protect' ) }
+
+
+ { createInterpolateElement(
+ __(
+ 'When enabled, users can only set passwords that meet strong security standards, helping protect their accounts and your site.',
+ 'jetpack-protect'
+ ),
+ {
+ link: , // TODO: Update this redirect URL
+ }
+ ) }
+
+
+
+ { createInterpolateElement(
+ __(
+ 'Jetpack recommends activating this setting. Please be mindful of the risks.',
+ 'jetpack-protect'
+ ),
+ {
+ link: , // TODO: Update this redirect URL
+ }
+ ) }
+
+
+
+ );
+
+ /**
+ * Render
+ */
+ return (
+
+
+
+
+
+ { accountProtectionSettings }
+ { isAccountProtectionEnabled && strictModeSettings }
+
+
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/projects/plugins/protect/src/js/routes/settings/styles.module.scss b/projects/plugins/protect/src/js/routes/settings/styles.module.scss
new file mode 100644
index 0000000000000..3b43e636ffd96
--- /dev/null
+++ b/projects/plugins/protect/src/js/routes/settings/styles.module.scss
@@ -0,0 +1,53 @@
+.container {
+ width: 100%;
+ max-width: calc( 744px + ( var( --spacing-base ) * 6 ) ); // 744px + 48px (desired inner width + horizontal padding)
+}
+
+.toggle-section {
+ display: flex;
+
+ &:not(:first-child) {
+ margin-top: calc( var( --spacing-base ) * 7 ); // 56px
+ }
+
+ &__control {
+ padding-top: calc( var( --spacing-base ) / 2 ); // 4px
+ margin-right: calc( var( --spacing-base ) * 2 ); // 16px
+
+ @media ( min-width: 600px ) {
+ margin-right: calc( var( --spacing-base ) * 5 ); // 48px
+ }
+ }
+
+ &__content {
+ width: 100%;
+ }
+
+ &__description {
+ a {
+ color: inherit;
+
+ &:hover {
+ color: var( --jp-black );
+ }
+ }
+ }
+
+ &__warning {
+ color: var( --jp-red-50 );
+
+ a {
+ color: var( --jp-red-50 );
+
+ &:hover {
+ color: var( --jp-red-70 )
+ }
+ }
+
+ svg {
+ fill: var( --jp-red-50 );
+ margin-bottom: calc( -1 * var( --spacing-base ) * 3/4 ); // -6px
+ margin-right: calc( var( --spacing-base ) / 4 ); // 2px
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/plugins/protect/src/js/types/account-protection.ts b/projects/plugins/protect/src/js/types/account-protection.ts
new file mode 100644
index 0000000000000..37d557638982b
--- /dev/null
+++ b/projects/plugins/protect/src/js/types/account-protection.ts
@@ -0,0 +1,12 @@
+export type AccountProtectionStatus = {
+ /** Whether the "account-protection" module is enabled. */
+ isEnabled: boolean;
+
+ /** The current Account Protetion settings. */
+ settings: AccountProtectionSettings;
+};
+
+export type AccountProtectionSettings = {
+ /** Whether the user has enabled strict mode. */
+ jetpackAccountProtectionStrictMode: boolean;
+};
diff --git a/projects/plugins/protect/src/js/types/global.d.ts b/projects/plugins/protect/src/js/types/global.d.ts
index 826b133869a7a..2f01fad2c7a54 100644
--- a/projects/plugins/protect/src/js/types/global.d.ts
+++ b/projects/plugins/protect/src/js/types/global.d.ts
@@ -29,6 +29,7 @@ declare global {
jetpackScan: ProductData;
hasPlan: boolean;
onboardingProgress: string[];
+ accountProtection: boolean;
waf: WafStatus;
};
}