secret_key = $this->get_secret_key(); } /** * Get or generate JWT secret key */ private function get_secret_key() { $key = get_option('twp_mobile_jwt_secret'); if (empty($key)) { // Generate a secure random key $key = bin2hex(random_bytes(32)); update_option('twp_mobile_jwt_secret', $key); } return $key; } /** * Register REST API endpoints */ public function register_endpoints() { add_action('rest_api_init', function() { // Login endpoint register_rest_route('twilio-mobile/v1', '/auth/login', array( 'methods' => 'POST', 'callback' => array($this, 'handle_login'), 'permission_callback' => '__return_true' )); // Refresh token endpoint register_rest_route('twilio-mobile/v1', '/auth/refresh', array( 'methods' => 'POST', 'callback' => array($this, 'handle_refresh'), 'permission_callback' => '__return_true' )); // Logout endpoint register_rest_route('twilio-mobile/v1', '/auth/logout', array( 'methods' => 'POST', 'callback' => array($this, 'handle_logout'), 'permission_callback' => array($this, 'verify_token') )); // Validate token endpoint (for debugging) register_rest_route('twilio-mobile/v1', '/auth/validate', array( 'methods' => 'GET', 'callback' => array($this, 'handle_validate'), 'permission_callback' => array($this, 'verify_token') )); }); } /** * Handle login request */ public function handle_login($request) { $username = $request->get_param('username'); $password = $request->get_param('password'); $fcm_token = $request->get_param('fcm_token'); // Optional $device_info = $request->get_param('device_info'); // Optional if (empty($username) || empty($password)) { return new WP_Error('missing_credentials', 'Username and password are required', array('status' => 400)); } // Authenticate user $user = wp_authenticate($username, $password); if (is_wp_error($user)) { return new WP_Error('invalid_credentials', 'Invalid username or password', array('status' => 401)); } // Check if user has phone agent capabilities if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) { return new WP_Error('insufficient_permissions', 'User does not have phone agent access', array('status' => 403)); } // Generate tokens $access_token = $this->generate_token($user->ID, 'access'); $refresh_token = $this->generate_token($user->ID, 'refresh'); // Store session in database $this->store_session($user->ID, $refresh_token, $fcm_token, $device_info); // Get user data $user_data = $this->get_user_data($user->ID); return new WP_REST_Response(array( 'success' => true, 'access_token' => $access_token, 'refresh_token' => $refresh_token, 'expires_in' => $this->token_expiry, 'user' => $user_data ), 200); } /** * Handle token refresh request */ public function handle_refresh($request) { $refresh_token = $request->get_param('refresh_token'); if (empty($refresh_token)) { return new WP_Error('missing_token', 'Refresh token is required', array('status' => 400)); } // Verify refresh token $payload = $this->decode_token($refresh_token); if (!$payload || $payload->type !== 'refresh') { return new WP_Error('invalid_token', 'Invalid refresh token', array('status' => 401)); } // Check if session exists and is valid global $wpdb; $table = $wpdb->prefix . 'twp_mobile_sessions'; $session = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE user_id = %d AND refresh_token = %s AND is_active = 1 AND expires_at > NOW()", $payload->user_id, $refresh_token )); if (!$session) { return new WP_Error('invalid_session', 'Session expired or invalid', array('status' => 401)); } // Generate new access token $access_token = $this->generate_token($payload->user_id, 'access'); // Update last_used timestamp $wpdb->update( $table, array('last_used' => current_time('mysql')), array('id' => $session->id), array('%s'), array('%d') ); return new WP_REST_Response(array( 'success' => true, 'access_token' => $access_token, 'expires_in' => $this->token_expiry ), 200); } /** * Handle logout request */ public function handle_logout($request) { $user_id = $this->get_current_user_id(); if (!$user_id) { return new WP_Error('unauthorized', 'Invalid token', array('status' => 401)); } // Get refresh token from request $refresh_token = $request->get_param('refresh_token'); global $wpdb; $table = $wpdb->prefix . 'twp_mobile_sessions'; if ($refresh_token) { // Invalidate specific session $wpdb->update( $table, array('is_active' => 0), array('user_id' => $user_id, 'refresh_token' => $refresh_token), array('%d'), array('%d', '%s') ); } else { // Invalidate all sessions for this user $wpdb->update( $table, array('is_active' => 0), array('user_id' => $user_id), array('%d'), array('%d') ); } return new WP_REST_Response(array( 'success' => true, 'message' => 'Logged out successfully' ), 200); } /** * Handle token validation request */ public function handle_validate($request) { $user_id = $this->get_current_user_id(); if (!$user_id) { return new WP_Error('unauthorized', 'Invalid token', array('status' => 401)); } $user_data = $this->get_user_data($user_id); return new WP_REST_Response(array( 'success' => true, 'valid' => true, 'user' => $user_data ), 200); } /** * Generate JWT token */ private function generate_token($user_id, $type = 'access') { $issued_at = time(); $expiry = $type === 'refresh' ? $this->refresh_expiry : $this->token_expiry; $payload = array( 'iat' => $issued_at, 'exp' => $issued_at + $expiry, 'user_id' => $user_id, 'type' => $type ); return $this->encode_token($payload); } /** * Simple JWT encoding (header.payload.signature) */ private function encode_token($payload) { $header = array('typ' => 'JWT', 'alg' => 'HS256'); $segments = array(); $segments[] = $this->base64url_encode(json_encode($header)); $segments[] = $this->base64url_encode(json_encode($payload)); $signing_input = implode('.', $segments); $signature = hash_hmac('sha256', $signing_input, $this->secret_key, true); $segments[] = $this->base64url_encode($signature); return implode('.', $segments); } /** * Simple JWT decoding */ private function decode_token($token) { $segments = explode('.', $token); if (count($segments) !== 3) { return false; } list($header_b64, $payload_b64, $signature_b64) = $segments; // Verify signature $signing_input = $header_b64 . '.' . $payload_b64; $signature = $this->base64url_decode($signature_b64); $expected_signature = hash_hmac('sha256', $signing_input, $this->secret_key, true); if (!hash_equals($signature, $expected_signature)) { return false; } // Decode payload $payload = json_decode($this->base64url_decode($payload_b64)); if (!$payload) { return false; } // Check expiration if (isset($payload->exp) && $payload->exp < time()) { return false; } return $payload; } /** * Base64 URL encode */ private function base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } /** * Base64 URL decode */ private function base64url_decode($data) { return base64_decode(strtr($data, '-_', '+/')); } /** * Verify token (permission callback) */ public function verify_token($request) { $auth_header = $request->get_header('Authorization'); if (empty($auth_header)) { return false; } // Extract token from "Bearer " if (preg_match('/Bearer\s+(.*)$/i', $auth_header, $matches)) { $token = $matches[1]; } else { return false; } $payload = $this->decode_token($token); if (!$payload || $payload->type !== 'access') { return false; } // Store user ID for later use $request->set_param('_twp_user_id', $payload->user_id); return true; } /** * Get current user ID from token */ public function get_current_user_id() { $request = rest_get_server()->get_request(); return $request->get_param('_twp_user_id'); } /** * Store session in database */ private function store_session($user_id, $refresh_token, $fcm_token = null, $device_info = null) { global $wpdb; $table = $wpdb->prefix . 'twp_mobile_sessions'; $wpdb->insert( $table, array( 'user_id' => $user_id, 'refresh_token' => $refresh_token, 'fcm_token' => $fcm_token, 'device_info' => $device_info, 'created_at' => current_time('mysql'), 'expires_at' => date('Y-m-d H:i:s', time() + $this->refresh_expiry), 'last_used' => current_time('mysql'), 'is_active' => 1 ), array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d') ); } /** * Get user data for response */ private function get_user_data($user_id) { $user = get_userdata($user_id); if (!$user) { return null; } // Get agent phone number $agent_number = get_user_meta($user_id, 'twp_agent_phone', true); // Get agent status global $wpdb; $status_table = $wpdb->prefix . 'twp_agent_status'; $status = $wpdb->get_row($wpdb->prepare( "SELECT status, is_logged_in, current_call_sid FROM $status_table WHERE user_id = %d", $user_id )); // Get user extension $ext_table = $wpdb->prefix . 'twp_user_extensions'; $extension = $wpdb->get_row($wpdb->prepare( "SELECT extension, direct_dial_number FROM $ext_table WHERE user_id = %d", $user_id )); return array( 'id' => $user->ID, 'username' => $user->user_login, 'display_name' => $user->display_name, 'email' => $user->user_email, 'phone_number' => $agent_number, 'extension' => $extension ? $extension->extension : null, 'direct_dial' => $extension ? $extension->direct_dial_number : null, 'status' => $status ? $status->status : 'offline', 'is_logged_in' => $status ? (bool)$status->is_logged_in : false, 'current_call_sid' => $status ? $status->current_call_sid : null, 'capabilities' => array( 'can_access_browser_phone' => user_can($user_id, 'twp_access_browser_phone'), 'can_access_voicemails' => user_can($user_id, 'twp_access_voicemails'), 'can_access_call_log' => user_can($user_id, 'twp_access_call_log'), 'can_access_agent_queue' => user_can($user_id, 'twp_access_agent_queue'), 'can_access_sms_inbox' => user_can($user_id, 'twp_access_sms_inbox'), 'is_admin' => user_can($user_id, 'manage_options') ) ); } /** * Update FCM token for existing session */ public function update_fcm_token($user_id, $refresh_token, $fcm_token) { global $wpdb; $table = $wpdb->prefix . 'twp_mobile_sessions'; $wpdb->update( $table, array('fcm_token' => $fcm_token), array('user_id' => $user_id, 'refresh_token' => $refresh_token, 'is_active' => 1), array('%s'), array('%d', '%s', '%d') ); } /** * Get all active FCM tokens for a user */ public function get_user_fcm_tokens($user_id) { global $wpdb; $table = $wpdb->prefix . 'twp_mobile_sessions'; return $wpdb->get_col($wpdb->prepare( "SELECT fcm_token FROM $table WHERE user_id = %d AND is_active = 1 AND fcm_token IS NOT NULL AND expires_at > NOW()", $user_id )); } /** * Clean up expired sessions */ public static function cleanup_expired_sessions() { global $wpdb; $table = $wpdb->prefix . 'twp_mobile_sessions'; $wpdb->query("UPDATE $table SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1"); } }