feat: SPA-based password reset page

- Created ResetPassword.tsx with:
  - Password reset form with strength indicator
  - Key validation on load
  - Show/hide password toggle
  - Success/error states
  - Redirect to login on success

- Updated EmailManager.php:
  - Changed reset_link from wp-login.php to SPA route
  - Format: /wp-admin/admin.php?page=woonoow#/reset-password?key=KEY&login=LOGIN

- Added AuthController API methods:
  - validate_reset_key: Validates reset key before showing form
  - reset_password: Performs actual password reset

- Registered new REST routes in Routes.php:
  - POST /auth/validate-reset-key
  - POST /auth/reset-password

Password reset emails now link to the SPA instead of native WordPress.
This commit is contained in:
Dwindi Ramadhana
2026-01-03 16:59:05 +07:00
parent 3f8d15de61
commit 316fcbf2f0
5 changed files with 372 additions and 5 deletions

View File

@@ -230,4 +230,100 @@ class AuthController {
'message' => __( 'Password reset email sent! Please check your inbox.', 'woonoow' ),
], 200 );
}
/**
* Validate password reset key
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response Response object
*/
public static function validate_reset_key( WP_REST_Request $request ): WP_REST_Response {
$key = sanitize_text_field( $request->get_param( 'key' ) );
$login = sanitize_text_field( $request->get_param( 'login' ) );
if ( empty( $key ) || empty( $login ) ) {
return new WP_REST_Response( [
'valid' => false,
'message' => __( 'Invalid password reset link', 'woonoow' ),
], 400 );
}
// Check the reset key
$user = check_password_reset_key( $key, $login );
if ( is_wp_error( $user ) ) {
$error_code = $user->get_error_code();
$message = __( 'This password reset link has expired or is invalid.', 'woonoow' );
if ( $error_code === 'invalid_key' ) {
$message = __( 'This password reset link is invalid.', 'woonoow' );
} elseif ( $error_code === 'expired_key' ) {
$message = __( 'This password reset link has expired. Please request a new one.', 'woonoow' );
}
return new WP_REST_Response( [
'valid' => false,
'message' => $message,
], 400 );
}
return new WP_REST_Response( [
'valid' => true,
'user' => [
'login' => $user->user_login,
'email' => $user->user_email,
],
], 200 );
}
/**
* Reset password with key
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response Response object
*/
public static function reset_password( WP_REST_Request $request ): WP_REST_Response {
$key = sanitize_text_field( $request->get_param( 'key' ) );
$login = sanitize_text_field( $request->get_param( 'login' ) );
$password = $request->get_param( 'password' );
if ( empty( $key ) || empty( $login ) || empty( $password ) ) {
return new WP_REST_Response( [
'success' => false,
'message' => __( 'Missing required fields', 'woonoow' ),
], 400 );
}
// Validate password strength
if ( strlen( $password ) < 8 ) {
return new WP_REST_Response( [
'success' => false,
'message' => __( 'Password must be at least 8 characters long', 'woonoow' ),
], 400 );
}
// Validate the reset key
$user = check_password_reset_key( $key, $login );
if ( is_wp_error( $user ) ) {
return new WP_REST_Response( [
'success' => false,
'message' => __( 'This password reset link has expired or is invalid. Please request a new one.', 'woonoow' ),
], 400 );
}
// Reset the password
reset_password( $user, $password );
// Delete the password reset key so it can't be reused
delete_user_meta( $user->ID, 'default_password_nag' );
// Trigger password changed action
do_action( 'password_reset', $user, $password );
return new WP_REST_Response( [
'success' => true,
'message' => __( 'Password reset successfully. You can now log in with your new password.', 'woonoow' ),
], 200 );
}
}

View File

@@ -79,6 +79,20 @@ class Routes {
'permission_callback' => '__return_true',
] );
// Validate password reset key (public)
register_rest_route( $namespace, '/auth/validate-reset-key', [
'methods' => 'POST',
'callback' => [ AuthController::class, 'validate_reset_key' ],
'permission_callback' => '__return_true',
] );
// Reset password with key (public)
register_rest_route( $namespace, '/auth/reset-password', [
'methods' => 'POST',
'callback' => [ AuthController::class, 'reset_password' ],
'permission_callback' => '__return_true',
] );
// Defer to controllers to register their endpoints
CheckoutController::register();
OrdersController::register();

View File

@@ -327,12 +327,12 @@ class EmailManager {
return $message; // Use WordPress default
}
// Build reset URL - use SPA route if available, otherwise WordPress default
$site_url = get_site_url();
// Build reset URL - use SPA route
$admin_url = admin_url('admin.php?page=woonoow');
// Check if this is a WooCommerce customer - use SPA reset page
// Otherwise fall back to WordPress default reset page
$reset_link = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'login');
// Build SPA reset password URL with hash router format
// Format: /wp-admin/admin.php?page=woonoow#/reset-password?key=KEY&login=LOGIN
$reset_link = $admin_url . '#/reset-password?key=' . $key . '&login=' . rawurlencode($user_login);
// Create a pseudo WC_Customer for template rendering
$customer = null;