<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\rest\RestRouteManager;
|
use JVBase\managers\MagicLinkManager;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Magic Link REST Routes
|
*
|
* Handles API endpoints for passwordless authentication
|
*/
|
class MagicLinkRoutes extends RestRouteManager
|
{
|
|
public function __construct()
|
{
|
parent::__construct();
|
}
|
|
/**
|
* Register REST routes
|
*/
|
public function registerRoutes(): void
|
{
|
// Send magic link
|
register_rest_route($this->namespace, '/magic', [
|
'methods' => 'POST',
|
'callback' => [$this, 'sendMagicLink'],
|
'permission_callback' => [$this, 'checkRateLimit'],
|
// 'args' => [
|
// 'email' => [
|
// 'required' => true,
|
// 'type' => 'string',
|
// 'format' => 'email',
|
// 'validate_callback' => function($param) {
|
// return is_email($param);
|
// }
|
// ],
|
// 'type' => [
|
// 'required' => false,
|
// 'type' => 'string',
|
// 'default' => 'login',
|
// 'enum' => ['login', 'signup', 'referral', 'reset']
|
// ],
|
// 'context' => [
|
// 'required' => false,
|
// 'type' => 'object',
|
// 'default' => []
|
// ]
|
// ]
|
]);
|
|
// Resend magic link
|
register_rest_route($this->namespace, '/magic/resend', [
|
'methods' => 'POST',
|
'callback' => [$this, 'resendMagicLink'],
|
'permission_callback' => [$this, 'checkRateLimit'],
|
'args' => [
|
'email' => [
|
'required' => true,
|
'type' => 'string',
|
'format' => 'email'
|
],
|
'type' => [
|
'required' => true,
|
'type' => 'string'
|
]
|
]
|
]);
|
|
// Check token validity (useful for frontend)
|
register_rest_route($this->namespace, '/magic/verify', [
|
'methods' => 'POST',
|
'callback' => [$this, 'verifyToken'],
|
'permission_callback' => '__return_true',
|
'args' => [
|
'token' => [
|
'required' => true,
|
'type' => 'string'
|
],
|
'email' => [
|
'required' => true,
|
'type' => 'string',
|
'format' => 'email'
|
]
|
]
|
]);
|
}
|
|
/**
|
* Send a magic link via email
|
*
|
* @param WP_REST_Request $request
|
* @return WP_REST_Response
|
*/
|
public function sendMagicLink(WP_REST_Request $request): WP_REST_Response
|
{
|
$data = $request->get_json_params();
|
|
// Verify Turnstile
|
if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) {
|
return $this->error('Security verification failed', 'turnstile_failed', 403);
|
}
|
$email = sanitize_email($request->get_param('email')??$request->get_param('user_email')??'');
|
$type = sanitize_text_field($request->get_param('type')) ?? MagicLinkManager::TYPE_LOGIN;
|
$context = $request->get_param('context') ?? [];
|
|
// Validate email
|
if (!is_email($email)) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'Invalid email address'
|
], 400);
|
}
|
|
// Check if email exists
|
$exists = email_exists($email);
|
if ($type === MagicLinkManager::TYPE_LOGIN && !$exists) {
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'Invalid email address'
|
]);
|
}
|
|
if ($type === MagicLinkManager::TYPE_SIGNUP && $exists) {
|
// Redirect to login instead
|
$type = MagicLinkManager::TYPE_LOGIN;
|
}
|
|
// Send the magic link
|
$result = JVB()->magicLink()?->sendMagicLink($email, $type, $context);
|
|
if (is_wp_error($result)) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => $result->get_error_message(),
|
'code' => $result->get_error_code()
|
], 400);
|
}
|
|
// Return success (never reveal if user exists or not)
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'If an account exists with this email, we\'ve sent a login link.'
|
], 200);
|
}
|
|
/**
|
* Resend a magic link
|
*
|
* @param WP_REST_Request $request
|
* @return WP_REST_Response
|
*/
|
public function resendMagicLink(WP_REST_Request $request): WP_REST_Response
|
{
|
// Same as sending, but could add additional logging
|
return $this->sendMagicLink($request);
|
}
|
|
/**
|
* Verify a token without consuming it
|
* Useful for frontend validation before redirect
|
*
|
* @param WP_REST_Request $request
|
* @return WP_REST_Response
|
*/
|
public function verifyToken(WP_REST_Request $request): WP_REST_Response
|
{
|
$token = sanitize_text_field($request->get_param('token'));
|
$email = sanitize_email($request->get_param('email'));
|
|
// This returns array|WP_Error - check for error first
|
$token_data = JVB()->magicLink()?->verifyToken($token, $email);
|
|
if (is_wp_error($token_data)) {
|
return new WP_REST_Response([
|
'valid' => false,
|
'message' => $token_data->get_error_message()
|
], 400);
|
}
|
|
// Now check the data
|
if (!isset($token_data['email']) || $token_data['email'] !== $email) {
|
return new WP_REST_Response([
|
'valid' => false,
|
'message' => 'Invalid token'
|
], 400);
|
}
|
|
// Check expiration - but your cache-based system doesn't store expires_at
|
// If token wasn't expired, it wouldn't have been returned from cache
|
// So just return valid:
|
return new WP_REST_Response([
|
'valid' => true,
|
'type' => $token_data['type'] ?? 'unknown'
|
], 200);
|
}
|
|
|
}
|