feat: Implement OAuth license activation flow

- Add LicenseConnect.tsx focused OAuth confirmation page in customer SPA
- Add /licenses/oauth/validate and /licenses/oauth/confirm API endpoints
- Update App.tsx to render license-connect outside BaseLayout (no header/footer)
- Add license_activation_method field to product settings in Admin SPA
- Create LICENSING_MODULE.md with comprehensive OAuth flow documentation
- Update API_ROUTES.md with license module endpoints
This commit is contained in:
Dwindi Ramadhana
2026-01-31 22:22:22 +07:00
parent d80f34c8b9
commit a0b5f8496d
23 changed files with 3218 additions and 806 deletions

View File

@@ -1,4 +1,5 @@
<?php
/**
* Licensing Module Bootstrap
*
@@ -12,79 +13,306 @@ if (!defined('ABSPATH')) exit;
use WooNooW\Core\ModuleRegistry;
use WooNooW\Modules\LicensingSettings;
class LicensingModule {
class LicensingModule
{
/**
* Initialize the licensing module
*/
public static function init() {
public static function init()
{
// Register settings schema
LicensingSettings::init();
// Initialize license manager immediately since we're already in plugins_loaded
// Note: This is called from woonoow.php inside plugins_loaded action,
// so we can call maybe_init_manager directly instead of scheduling another hook
self::maybe_init_manager();
// Install tables on module enable
add_action('woonoow/module/enabled', [__CLASS__, 'on_module_enabled']);
// Add product meta fields
add_action('woocommerce_product_options_general_product_data', [__CLASS__, 'add_product_licensing_fields']);
add_action('woocommerce_process_product_meta', [__CLASS__, 'save_product_licensing_fields']);
// License Connect OAuth endpoint
add_action('init', [__CLASS__, 'register_license_connect_endpoint']);
add_action('template_redirect', [__CLASS__, 'handle_license_connect'], 5);
}
/**
* Initialize manager if module is enabled
*/
public static function maybe_init_manager() {
public static function maybe_init_manager()
{
if (ModuleRegistry::is_enabled('licensing')) {
// Ensure tables exist
self::ensure_tables();
LicenseManager::init();
}
}
/**
* Ensure database tables exist
*/
private static function ensure_tables() {
private static function ensure_tables()
{
global $wpdb;
$table = $wpdb->prefix . 'woonoow_licenses';
// Check if table exists
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
LicenseManager::create_tables();
}
}
/**
* Handle module enable
*/
public static function on_module_enabled($module_id) {
public static function on_module_enabled($module_id)
{
if ($module_id === 'licensing') {
LicenseManager::create_tables();
}
}
/**
* Register license connect rewrite endpoint
*/
public static function register_license_connect_endpoint()
{
add_rewrite_endpoint('license-connect', EP_ROOT | EP_PAGES);
}
/**
* Handle license-connect endpoint (OAuth confirmation page)
*/
public static function handle_license_connect()
{
// Parse the request URI to check if this is the license-connect page
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
$parsed_path = parse_url($request_uri, PHP_URL_PATH);
// Check if path contains license-connect
if (strpos($parsed_path, '/license-connect') === false) {
return;
}
// Get parameters
$license_key = sanitize_text_field($_GET['license_key'] ?? '');
$site_url = esc_url_raw($_GET['site_url'] ?? '');
$return_url = esc_url_raw($_GET['return_url'] ?? '');
$state = sanitize_text_field($_GET['state'] ?? '');
$action = sanitize_text_field($_GET['action'] ?? '');
// Handle form submission (confirmation)
if ($action === 'confirm' && !empty($_POST['confirm_license'])) {
self::process_license_confirmation();
return;
}
// Require login
if (!is_user_logged_in()) {
$login_url = wp_login_url(add_query_arg($_GET, home_url('/my-account/license-connect/')));
wp_redirect($login_url);
exit;
}
// Validate parameters
if (empty($license_key) || empty($site_url) || empty($state)) {
self::render_license_connect_page([
'error' => __('Invalid license connection request. Missing required parameters.', 'woonoow'),
]);
return;
}
// Verify state token
$state_data = LicenseManager::verify_oauth_state($state);
if (!$state_data) {
self::render_license_connect_page([
'error' => __('Invalid or expired connection request. Please try again.', 'woonoow'),
]);
return;
}
// Get license and verify ownership
$license = LicenseManager::get_license_by_key($license_key);
if (!$license) {
self::render_license_connect_page([
'error' => __('License key not found.', 'woonoow'),
]);
return;
}
// Verify license belongs to current user
$current_user_id = get_current_user_id();
if ((int)$license['user_id'] !== $current_user_id) {
self::render_license_connect_page([
'error' => __('This license does not belong to your account.', 'woonoow'),
]);
return;
}
// Check license status
if ($license['status'] !== 'active') {
self::render_license_connect_page([
'error' => __('This license is not active.', 'woonoow'),
]);
return;
}
// Check activation limit
if ($license['activation_limit'] > 0 && $license['activation_count'] >= $license['activation_limit']) {
self::render_license_connect_page([
'error' => sprintf(
__('Activation limit reached (%d/%d sites).', 'woonoow'),
$license['activation_count'],
$license['activation_limit']
),
]);
return;
}
// Get product info
$product = wc_get_product($license['product_id']);
$product_name = $product ? $product->get_name() : __('Unknown Product', 'woonoow');
// Render confirmation page
self::render_license_connect_page([
'license' => $license,
'product_name' => $product_name,
'site_url' => $site_url,
'return_url' => $return_url,
'state' => $state,
]);
}
/**
* Process license confirmation form submission
*/
private static function process_license_confirmation()
{
if (!is_user_logged_in()) {
wp_die(__('You must be logged in.', 'woonoow'));
}
// Verify nonce
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'woonoow_license_connect')) {
wp_die(__('Security check failed.', 'woonoow'));
}
$license_key = sanitize_text_field($_POST['license_key'] ?? '');
$site_url = esc_url_raw($_POST['site_url'] ?? '');
$return_url = esc_url_raw($_POST['return_url'] ?? '');
$state = sanitize_text_field($_POST['state'] ?? '');
// Verify state
$state_data = LicenseManager::verify_oauth_state($state);
if (!$state_data) {
wp_die(__('Invalid or expired request.', 'woonoow'));
}
// Get and verify license
$license = LicenseManager::get_license_by_key($license_key);
if (!$license || (int)$license['user_id'] !== get_current_user_id()) {
wp_die(__('Invalid license.', 'woonoow'));
}
// Generate activation token
$token_data = LicenseManager::generate_activation_token($license['id'], $site_url);
// Build return URL with token
$callback_url = add_query_arg([
'activation_token' => $token_data['token'],
'license_key' => $license_key,
'state' => $state,
], $return_url);
// Redirect back to client site
wp_redirect($callback_url);
exit;
}
/**
* Render license connect confirmation page
*/
private static function render_license_connect_page($args)
{
// Set headers
status_header(200);
nocache_headers();
// Include WP header
get_header('woonoow');
echo '<div class="woonoow-license-connect" style="max-width: 600px; margin: 40px auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', sans-serif;">';
if (!empty($args['error'])) {
echo '<div style="background: #fee; border: 1px solid #c00; padding: 15px; border-radius: 4px; margin-bottom: 20px;">';
echo '<strong>' . esc_html__('Error', 'woonoow') . ':</strong> ' . esc_html($args['error']);
echo '</div>';
echo '<a href="' . esc_url(home_url()) . '" style="color: #0073aa;">&larr; ' . esc_html__('Return Home', 'woonoow') . '</a>';
} else {
$license = $args['license'];
$activations_remaining = $license['activation_limit'] > 0
? $license['activation_limit'] - $license['activation_count']
: '∞';
echo '<h1 style="font-size: 24px; margin-bottom: 20px;">' . esc_html__('Connect Site to License', 'woonoow') . '</h1>';
echo '<div style="background: #f8f9fa; border: 1px solid #e5e7eb; padding: 20px; border-radius: 8px; margin-bottom: 20px;">';
echo '<table style="width: 100%; border-collapse: collapse;">';
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('Site', 'woonoow') . ':</td><td style="padding: 8px 0; font-weight: 600;">' . esc_html($args['site_url']) . '</td></tr>';
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('Product', 'woonoow') . ':</td><td style="padding: 8px 0; font-weight: 600;">' . esc_html($args['product_name']) . '</td></tr>';
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('License', 'woonoow') . ':</td><td style="padding: 8px 0; font-family: monospace;">' . esc_html($license['license_key']) . '</td></tr>';
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('Activations', 'woonoow') . ':</td><td style="padding: 8px 0;">' . esc_html($license['activation_count']) . '/' . ($license['activation_limit'] ?: '∞') . ' ' . esc_html__('used', 'woonoow') . '</td></tr>';
echo '</table>';
echo '</div>';
echo '<form method="post" action="' . esc_url(add_query_arg('action', 'confirm', home_url('/my-account/license-connect/'))) . '">';
echo wp_nonce_field('woonoow_license_connect', '_wpnonce', true, false);
echo '<input type="hidden" name="license_key" value="' . esc_attr($license['license_key']) . '">';
echo '<input type="hidden" name="site_url" value="' . esc_attr($args['site_url']) . '">';
echo '<input type="hidden" name="return_url" value="' . esc_attr($args['return_url']) . '">';
echo '<input type="hidden" name="state" value="' . esc_attr($args['state']) . '">';
echo '<div style="display: flex; gap: 10px;">';
echo '<button type="submit" name="confirm_license" value="1" style="background: #2563eb; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer;">';
echo esc_html__('Connect This Site', 'woonoow');
echo '</button>';
echo '<a href="' . esc_url($args['return_url'] ?: home_url()) . '" style="background: #e5e7eb; color: #374151; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; text-decoration: none;">';
echo esc_html__('Cancel', 'woonoow');
echo '</a>';
echo '</div>';
echo '</form>';
}
echo '</div>';
get_footer('woonoow');
exit;
}
/**
* Add licensing fields to product edit page
*/
public static function add_product_licensing_fields() {
public static function add_product_licensing_fields()
{
global $post;
if (!ModuleRegistry::is_enabled('licensing')) {
return;
}
echo '<div class="options_group show_if_simple show_if_downloadable">';
woocommerce_wp_checkbox([
'id' => '_woonoow_licensing_enabled',
'label' => __('Enable Licensing', 'woonoow'),
'description' => __('Generate license keys for this product on purchase', 'woonoow'),
]);
woocommerce_wp_text_input([
'id' => '_woonoow_license_activation_limit',
'label' => __('Activation Limit', 'woonoow'),
@@ -95,7 +323,7 @@ class LicensingModule {
'step' => '1',
],
]);
woocommerce_wp_text_input([
'id' => '_woonoow_license_expiry_days',
'label' => __('License Expiry (Days)', 'woonoow'),
@@ -106,23 +334,48 @@ class LicensingModule {
'step' => '1',
],
]);
// Only show activation method if per-product override is enabled
$licensing_settings = get_option('woonoow_module_licensing_settings', []);
$allow_override = $licensing_settings['allow_product_override'] ?? false;
if ($allow_override) {
woocommerce_wp_select([
'id' => '_woonoow_license_activation_method',
'label' => __('Activation Method', 'woonoow'),
'description' => __('Override site-level setting for this product', 'woonoow'),
'options' => [
'' => __('Use Site Default', 'woonoow'),
'api' => __('Simple API (license key only)', 'woonoow'),
'oauth' => __('Secure OAuth (requires account login)', 'woonoow'),
],
]);
}
echo '</div>';
}
/**
* Save licensing fields
*/
public static function save_product_licensing_fields($post_id) {
public static function save_product_licensing_fields($post_id)
{
$licensing_enabled = isset($_POST['_woonoow_licensing_enabled']) ? 'yes' : 'no';
update_post_meta($post_id, '_woonoow_licensing_enabled', $licensing_enabled);
if (isset($_POST['_woonoow_license_activation_limit'])) {
update_post_meta($post_id, '_woonoow_license_activation_limit', absint($_POST['_woonoow_license_activation_limit']));
}
if (isset($_POST['_woonoow_license_expiry_days'])) {
update_post_meta($post_id, '_woonoow_license_expiry_days', absint($_POST['_woonoow_license_expiry_days']));
}
if (isset($_POST['_woonoow_license_activation_method'])) {
$method = $_POST['_woonoow_license_activation_method'];
// Accept empty (site default), api, or oauth
if ($method === '' || in_array($method, ['api', 'oauth'])) {
update_post_meta($post_id, '_woonoow_license_activation_method', sanitize_key($method));
}
}
}
}