diff --git a/.distignore b/.distignore new file mode 100644 index 00000000..51f9f111 --- /dev/null +++ b/.distignore @@ -0,0 +1,99 @@ +# Directories +/.wordpress-org +/.git +/.github +/.husky +/node_modules +/admin/ui/node_modules +/tests +/scripts +/.vscode +/.idea +/stubs + +# Hidden files +.distignore +.gitignore +.gitattributes +.editorconfig +.eslintrc +.eslintignore +.prettierrc +.prettierignore +.phpunit.result.cache +.actrc +.secrets +.env +.DS_Store + +# Development files +CLAUDE.md +CLAUDE.local.md +.claude +DEVELOPMENT.md +README.md +CHANGELOG.md +CONTRIBUTING.md + +# Configuration files +composer.json +composer.lock +package.json +package-lock.json +phpunit.xml +phpunit.xml.dist +phpcs.xml +phpcs.xml.dist +.wp-env.json +vite.config.ts +tsconfig.json +tsconfig.node.json +tailwind.config.js +postcss.config.js +vitest.config.ts +components.json +eslint.config.js + +# Build tools +Gruntfile.js +gulpfile.js +webpack.config.js + +# Testing +/tests +phpunit.xml +phpunit.xml.dist + +# Documentation +/docs +*.md + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Temporary files +*.tmp +*.temp +*.swp +*~ +.tmp + +# Archives +*.zip +*.tar +*.gz + +# OS files +Thumbs.db +Desktop.ini + +# Editor files +*.sublime-project +*.sublime-workspace + +# PHP files that shouldn't be in frontend dist +admin/ui/dist/class-jwt-auth-public.php +admin/ui/dist/index.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..17dbbc32 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,jsx,ts,tsx,json,css,scss}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..17e99a9a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: tmeister diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..98f1e2b1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..8b6cfc8a --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,43 @@ +# Issue Name +## Prerequisites + +Please answer the following questions for yourself before submitting an issue. + +- [ ] I am running the latest plugin version +- [ ] I am running the latest WordPress version +- [ ] I know what PHP version I'm using +- [ ] I checked the documentation and found no answer +- [ ] I checked to make sure that this issue has not already been filed + +## Context + +Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. + +* WordPress version +* PHP Version +* Plugin A name and version +* Plugin B name and version + +## Expected Behavior + +Please describe the behavior you are expecting. + +## Current Behavior + +What is the current behavior? + +## Failure Information (for bugs) + +Please help provide information about the failure if this is a bug. If it is not a bug, please remove the rest of this template. + +### Steps to Reproduce + +Please provide detailed steps for reproducing the issue. + +1. Step 1 +2. Step 2 +3. You get it... + +### Failure Logs + +Please include any relevant log snippets or files here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..6fe27dcf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +# Description + +Please include a summary of the change or changes and which issue is fixed. Please also include relevant motivation and context. + +Fixes #(issue) + +## Type of change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) +- [ ] This change requires a documentation update + +# How has this been tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration and if you use specific plugins. + +- [ ] Describe test A +- [ ] Describe test B + +**Test Configuration**: +* WordPress version +* PHP version +* Plugin name and version +* Plugin B name and version + +# Checklist: + +- [ ] My code follows the style guidelines of this project (WordPress code standards) +- [ ] I have performed a self-review of my own code +- [ ] I have commented on my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation (if needed) +- [ ] My changes generate no new warnings +- [ ] I have described how I made my tests that prove my fix is effective or that my feature works diff --git a/.github/workflows/test-github-release.yml b/.github/workflows/test-github-release.yml new file mode 100644 index 00000000..7d616d9a --- /dev/null +++ b/.github/workflows/test-github-release.yml @@ -0,0 +1,62 @@ +name: Deploy Test Release + +on: + push: + branches: + - test-release + workflow_dispatch: # Allow manual triggering + +jobs: + build-and-test: + name: Build and Test Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: composer + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install PHP dependencies (production only) + run: composer install --no-dev --optimize-autoloader --prefer-dist --no-interaction + + - name: Install Node.js dependencies + run: npm ci + + - name: Build frontend assets + run: npm run build + + - name: Run tests + run: | + chmod +x scripts/run-tests.sh + ./scripts/run-tests.sh + + - name: Reinstall production dependencies + run: | + echo "Reinstalling production-only composer dependencies just in case..." + composer install --no-dev --optimize-autoloader --prefer-dist --no-interaction + + - name: Generate zip file + run: | + chmod +x scripts/create-release.sh + ./scripts/create-release.sh + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + tag_name: test-${{ github.run_number }} + name: JWT Auth Test Release #${{ github.run_number }} + files: | + wp-api-jwt-auth.zip + draft: true + prerelease: true + generate_release_notes: true diff --git a/.github/workflows/test-wordpress-deploy.yml b/.github/workflows/test-wordpress-deploy.yml new file mode 100644 index 00000000..e44e1500 --- /dev/null +++ b/.github/workflows/test-wordpress-deploy.yml @@ -0,0 +1,59 @@ +name: Test WordPress.org Deploy (Dry Run) + +on: + push: + branches: + - build-test + workflow_dispatch: # Allow manual triggering + +jobs: + test-wordpress-deploy: + name: Test WordPress Deploy (Dry Run) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: composer + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install PHP dependencies (production only) + run: composer install --no-dev --optimize-autoloader --prefer-dist --no-interaction + + - name: Install Node.js dependencies + run: npm ci + + - name: Build frontend assets + run: npm run build + + - name: Run tests + run: | + chmod +x scripts/run-tests.sh + ./scripts/run-tests.sh + + - name: Reinstall production dependencies + run: | + echo "Reinstalling production-only composer dependencies..." + composer install --no-dev --optimize-autoloader --prefer-dist --no-interaction + + - name: Build WordPress Plugin Zip + uses: 10up/action-wordpress-plugin-build-zip@stable + with: + retention-days: 1 + env: + SLUG: jwt-authentication-for-wp-rest-api + + - name: Display build info + run: | + echo "đŸ“Ļ Plugin ZIP has been built and uploaded as an artifact" + echo "đŸ—“ī¸ The artifact will be retained for 1 day" + echo "â„šī¸ Download the artifact to test the plugin package" diff --git a/.github/workflows/wordpress-assets-update.yml b/.github/workflows/wordpress-assets-update.yml new file mode 100644 index 00000000..e0f968fd --- /dev/null +++ b/.github/workflows/wordpress-assets-update.yml @@ -0,0 +1,21 @@ +name: Plugin asset/readme update +on: + push: + branches: + - trunk +jobs: + trunk: + name: Push to trunk + runs-on: ubuntu-latest + steps: + - name: Install Subversion and rsync + run: sudo apt-get update && sudo apt-get install -y subversion rsync + - name: Checkout code + uses: actions/checkout@master + - name: WordPress.org plugin asset/readme update + uses: 10up/action-wordpress-plugin-asset-update@stable + env: + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SLUG: jwt-authentication-for-wp-rest-api + IGNORE_OTHER_FILES: true diff --git a/.github/workflows/wordpress-release.yml b/.github/workflows/wordpress-release.yml new file mode 100644 index 00000000..2b84a7b6 --- /dev/null +++ b/.github/workflows/wordpress-release.yml @@ -0,0 +1,55 @@ +name: Deploy to WordPress.org + +on: + push: + tags: + - "*" + +jobs: + tag: + name: New tag + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: composer + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install PHP dependencies (production only) + run: composer install --no-dev --optimize-autoloader --prefer-dist --no-interaction + + - name: Install Node.js dependencies + run: npm ci + + - name: Build frontend assets + run: npm run build + + - name: Run tests + run: | + chmod +x scripts/run-tests.sh + ./scripts/run-tests.sh + + - name: Reinstall production dependencies + run: | + echo "Reinstalling production-only composer dependencies..." + composer install --no-dev --optimize-autoloader --prefer-dist --no-interaction + + - name: Install Subversion and rsync + run: sudo apt-get update && sudo apt-get install -y subversion rsync + + - name: WordPress Plugin Deploy + uses: 10up/action-wordpress-plugin-deploy@stable + env: + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SLUG: jwt-authentication-for-wp-rest-api diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bf5e119a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# IDE settings (keep project-wide settings, ignore personal ones) +.vscode/launch.json +.vscode/tasks.json +.vscode/*.code-workspace +.idea/ +.claude/ +stubs + +# Development documentation +DEVELOPMENT.md +.phpunit.result.cache + +# System files +.DS_Store +Thumbs.db + +# Dependencies +includes/vendor/ +admin/ui/node_modules +node_modules + +# Build outputs +admin/ui/dist +.tmp + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Act (GitHub Actions local testing) +.actrc +.secrets +.env diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..27b5ec00 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +#!/bin/sh +# Run linting before commit +npm run lint:fix diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..b6c98e52 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,42 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +build/ +admin/ui/dist/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Compiled binary addons +build/Release + +# Lock files +package-lock.json +yarn.lock + +# Temporary folders +.tmp/ +temp/ + +# Generated files +*.generated.* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..6af2de4f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "proseWrap": "preserve" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..8043a483 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,111 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "typescript.preferences.importModuleSpecifier": "relative", + "files.associations": { + "*.css": "tailwindcss" + }, + "emmet.includeLanguages": { + "typescript": "html", + "typescriptreact": "html" + }, + "intelephense.stubs": [ + "apache", + "bcmath", + "bz2", + "calendar", + "com_dotnet", + "Core", + "ctype", + "curl", + "date", + "dba", + "dom", + "enchant", + "exif", + "FFI", + "fileinfo", + "filter", + "fpm", + "ftp", + "gd", + "gettext", + "gmp", + "hash", + "iconv", + "imap", + "intl", + "json", + "ldap", + "libxml", + "mbstring", + "meta", + "mysqli", + "oci8", + "odbc", + "openssl", + "pcntl", + "pcre", + "PDO", + "pgsql", + "Phar", + "posix", + "pspell", + "readline", + "Reflection", + "session", + "shmop", + "SimpleXML", + "snmp", + "soap", + "sockets", + "sodium", + "SPL", + "sqlite3", + "standard", + "superglobals", + "sysvmsg", + "sysvsem", + "sysvshm", + "tidy", + "tokenizer", + "xml", + "xmlreader", + "xmlrpc", + "xmlwriter", + "xsl", + "Zend OPcache", + "zip", + "zlib", + "wordpress", + "random", + ], + "intelephense.environment.includePaths": [ + "includes/vendor/php-stubs/wordpress-stubs", + "/Users/tmeister/Code/Sources/wp", + "stubs" + ], + "php.stubs": [ + "wordpress", + "*", + "wordpress-globals", + "wordpress-constants", + "wordpress-functions", + "wordpress-hooks", + "wordpress-classes", + "wordpress-interfaces", + "wordpress-enums", + "wordpress-phpunit", + "wordpress-rest-api", + "wordpress-wp-cli", + ] +} diff --git a/.wordpress-org/banner-1544x500.jpg b/.wordpress-org/banner-1544x500.jpg new file mode 100644 index 00000000..68e50208 Binary files /dev/null and b/.wordpress-org/banner-1544x500.jpg differ diff --git a/.wordpress-org/banner-772x250.jpg b/.wordpress-org/banner-772x250.jpg new file mode 100644 index 00000000..93407d07 Binary files /dev/null and b/.wordpress-org/banner-772x250.jpg differ diff --git a/.wordpress-org/icon-128x128.jpg b/.wordpress-org/icon-128x128.jpg new file mode 100644 index 00000000..c4579c7f Binary files /dev/null and b/.wordpress-org/icon-128x128.jpg differ diff --git a/.wordpress-org/icon-256x256.jpg b/.wordpress-org/icon-256x256.jpg new file mode 100644 index 00000000..706ab2a8 Binary files /dev/null and b/.wordpress-org/icon-256x256.jpg differ diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 00000000..2cd5f5c8 --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,17 @@ +{ + "phpVersion": "8.1", + "plugins": ["."], + "config": { + "JWT_AUTH_SECRET_KEY": "your-development-secret-key-for-testing-purposes-only", + "JWT_AUTH_CORS_ENABLE": true, + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false + }, + "port": 8888, + "testsPort": 8889, + "themes": [], + "mappings": { + "wp-content/plugins/wp-api-jwt-auth": "." + } +} diff --git a/README.md b/README.md index 40e6f29c..08a4626d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,56 @@ A simple plugin to add [JSON Web Token (JWT)](https://tools.ietf.org/html/rfc751 To know more about JSON Web Tokens, please visit [http://jwt.io](http://jwt.io). +## Description + +This plugin seamlessly extends the WP REST API, enabling robust and secure authentication using JSON Web Tokens (JWT). It provides a straightforward way to authenticate users via the REST API, returning a standard JWT upon successful login. + +### Key features of this free version include: + +- **Standard JWT Authentication:** Implements the industry-standard [RFC 7519](https://tools.ietf.org/html/rfc7519) for secure claims representation. +- **Simple Endpoints:** Offers clear `/token` and `/token/validate` endpoints for generating and validating tokens. +- **Configurable Secret Key:** Define your unique secret key via `wp-config.php` for secure token signing. +- **Optional CORS Support:** Easily enable Cross-Origin Resource Sharing support via a `wp-config.php` constant. +- **Developer Hooks:** Provides filters (`jwt_auth_expire`, `jwt_auth_token_before_sign`, etc.) for customizing token behavior. + +For users requiring more advanced capabilities such as multiple signing algorithms (RS256, ES256), token refresh/revocation, UI-based configuration, or priority support, consider checking out **[JWT Authentication PRO](https://jwtauth.pro/?utm_source=github_readme&utm_medium=link&utm_campaign=pro_promotion&utm_content=description_link)**. + +**Support and Requests:** Please use [GitHub Issues](https://github.com/Tmeister/wp-api-jwt-auth/issues). For priority support, consider upgrading to [PRO](https://jwtauth.pro/?utm_source=github_readme&utm_medium=link&utm_campaign=pro_promotion&utm_content=description_support_link). + +## JWT Authentication PRO + +Elevate your WordPress security and integration capabilities with **JWT Authentication PRO**. Building upon the solid foundation of the free version, the PRO version offers advanced features, enhanced security options, and a streamlined user experience: + +- **Easy Configuration UI:** Manage all settings directly from the WordPress admin area. +- **Token Refresh Endpoint:** Allow users to refresh expired tokens seamlessly without requiring re-login. +- **Token Revocation Endpoint:** Immediately invalidate specific tokens for enhanced security control. +- **Customizable Token Payload:** Add custom claims to your JWT payload to suit your specific application needs. +- **Granular CORS Control:** Define allowed origins and headers with more precision directly in the settings. +- **Rate Limiting:** Protect your endpoints from abuse with configurable rate limits. +- **Audit Logs:** Keep track of token generation, validation, and errors. +- **Priority Support:** Get faster, dedicated support directly from the developer. + +**[Upgrade to JWT Authentication PRO Today!](https://jwtauth.pro/?utm_source=github_readme&utm_medium=link&utm_campaign=pro_promotion&utm_content=pro_section_cta)** + +### Free vs. PRO Comparison + +Here's a quick look at the key differences: + +| Feature | Free Version | JWT Auth Pro (starts at $59/yr) | +| -------------------------- | -------------------- | ------------------------------- | +| Basic JWT Authentication | ✅ Included | ✅ Included | +| Token Generation | ✅ Included | ✅ Included | +| Token Validation | ✅ Included | ✅ Included | +| Token Refresh Mechanism | ❌ Not Included | ✅ Included | +| Token Revocation | ❌ Not Included | ✅ Included | +| Token Management Dashboard | ❌ Not Included | ✅ Included | +| Analytics & Monitoring | ❌ Not Included | ✅ Included | +| Geo-IP Identification | ❌ Not Included | ✅ Included | +| Rate Limiting | ❌ Not Included | ✅ Included | +| Detailed Documentation | Basic | Comprehensive | +| Developer Tools | ❌ Not Included | ✅ Included | +| Premium Support | Community via GitHub | Priority Direct Support | + ## Requirements ### WP REST API V2 @@ -14,17 +64,17 @@ So, to use the **wp-api-jwt-auth** you need to install and activate [WP REST API ### PHP -**Minimum PHP version: 5.3.0** +**Minimum PHP version: 7.4.0** -### Enable PHP HTTP Authorization Header +### Enable PHP HTTP Authorization Header #### Shared Hosts -Most shared hosts have disabled the **HTTP Authorization Header** by default. +Most shared hosting providers have disabled the **HTTP Authorization Header** by default. To enable this option you'll need to edit your **.htaccess** file by adding the following: -``` +```apache RewriteEngine on RewriteCond %{HTTP:Authorization} ^(.*) RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] @@ -32,109 +82,103 @@ RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] #### WPEngine -To enable this option you'll need to edit your **.htaccess** file by adding the following (see https://github.com/Tmeister/wp-api-jwt-auth/issues/1): +For WPEngine hosting, you'll need to edit your **.htaccess** file by adding the following: -``` +```apache SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 ``` +See https://github.com/Tmeister/wp-api-jwt-auth/issues/1 for more details. + ## Installation & Configuration [Download the zip file](https://github.com/Tmeister/wp-api-jwt-auth/archive/master.zip) and install it like any other WordPress plugin. Or clone this repo into your WordPress installation into the wp-content/plugins folder. -### Configurate the Secret Key +### Configure the Secret Key The JWT needs a **secret key** to sign the token. This **secret key** must be unique and never revealed. -To add the **secret key**, edit your wp-config.php file and add a new constant called **JWT_AUTH_SECRET_KEY**. - +To add the **secret key**, edit your wp-config.php file and add a new constant called **JWT_AUTH_SECRET_KEY**: ```php define('JWT_AUTH_SECRET_KEY', 'your-top-secret-key'); ``` -You can use a string from here https://api.wordpress.org/secret-key/1.1/salt/ +You can generate a secure key from: https://api.wordpress.org/secret-key/1.1/salt/ -### Configurate CORs Support +**Looking for easier configuration?** [JWT Authentication PRO](https://jwtauth.pro/?utm_source=github_readme&utm_medium=link&utm_campaign=pro_promotion&utm_content=config_secret_key_link) allows you to manage all settings through a simple admin UI. -The **wp-api-jwt-auth** plugin has the option to activate [CORs](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support. +### Configure CORS Support -To enable the CORs Support edit your wp-config.php file and add a new constant called **JWT_AUTH_CORS_ENABLE** +The **wp-api-jwt-auth** plugin has the option to activate [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support. +To enable CORS Support, edit your wp-config.php file and add a new constant called **JWT_AUTH_CORS_ENABLE**: ```php define('JWT_AUTH_CORS_ENABLE', true); ``` - -Finally activate the plugin within the plugin dashboard. +Finally, activate the plugin within your wp-admin. ## Namespace and Endpoints -When the plugin is activated, a new namespace is added. - +When the plugin is activated, a new namespace is added: ``` /jwt-auth/v1 ``` +Also, two new endpoints are added to this namespace: -Also, two new endpoints are added to this namespace. - +| Endpoint | HTTP Verb | +| ------------------------------------- | --------- | +| _/wp-json/jwt-auth/v1/token_ | POST | +| _/wp-json/jwt-auth/v1/token/validate_ | POST | -Endpoint | HTTP Verb ---- | --- -*/wp-json/jwt-auth/v1/token* | POST -*/wp-json/jwt-auth/v1/token/validate* | POST +**Need more functionality?** [JWT Authentication PRO](https://jwtauth.pro/?utm_source=github_readme&utm_medium=link&utm_campaign=pro_promotion&utm_content=endpoints_pro_note) includes additional endpoints for token refresh and revocation. ## Usage + ### /wp-json/jwt-auth/v1/token -This is the entry point for the JWT Authentication. +This is the entry point for JWT Authentication. -Validates the user credentials, *username* and *password*, and returns a token to use in a future request to the API if the authentication is correct or error if the authentication fails. +It validates the user credentials, _username_ and _password_, and returns a token to use in future requests to the API if the authentication is correct, or an error if authentication fails. -#### Sample request using AngularJS +#### Sample Request Using AngularJS ```javascript +;(function () { + var app = angular.module('jwtAuth', []) -( function() { - var app = angular.module( 'jwtAuth', [] ); - - app.controller( 'MainController', function( $scope, $http ) { + app.controller('MainController', function ($scope, $http) { + var apiHost = 'http://yourdomain.com/wp-json' - var apiHost = 'http://yourdomain.com/wp-json'; - - $http.post( apiHost + '/jwt-auth/v1/token', { + $http + .post(apiHost + '/jwt-auth/v1/token', { username: 'admin', - password: 'password' - } ) - - .then( function( response ) { - console.log( response.data ) - } ) - - .catch( function( error ) { - console.error( 'Error', error.data[0] ); - } ); - - } ); - -} )(); - - + password: 'password', + }) + .then(function (response) { + console.log(response.data) + }) + .catch(function (error) { + console.error('Error', error.data[0]) + }) + }) +})() ``` Success response from the server: ```json { - "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9qd3QuZGV2IiwiaWF0IjoxNDM4NTcxMDUwLCJuYmYiOjE0Mzg1NzEwNTAsImV4cCI6MTQzOTE3NTg1MCwiZGF0YSI6eyJ1c2VyIjp7ImlkIjoiMSJ9fX0.YNe6AyWW4B7ZwfFE5wJ0O6qQ8QFcYizimDmBy6hCH_8", - "user_display_name": "admin", - "user_email": "admin@localhost.dev", - "user_nicename": "admin" + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9qd3QuZGV2IiwiaWF0IjoxNDM4NTcxMDUwLCJuYmYiOjE0Mzg1NzEwNTAsImV4cCI6MTQzOTE3NTg1MCwiZGF0YSI6eyJ1c2VyIjp7ImlkIjoiMSJ9fX0.YNe6AyWW4B7ZwfFE5wJ0O6qQ8QFcYizimDmBy6hCH_8", + "user_display_name": "admin", + "user_email": "admin@localhost.dev", + "user_nicename": "admin" } ``` @@ -142,55 +186,60 @@ Error response from the server: ```json { - "code": "jwt_auth_failed", - "data": { - "status": 403 - }, - "message": "Invalid Credentials." + "code": "jwt_auth_failed", + "data": { + "status": 403 + }, + "message": "Invalid Credentials." } ``` -Once you get the token, you must store it somewhere in your application, e.g. in a **cookie** or using **localstorage**. +Once you get the token, you must store it somewhere in your application, e.g. in a **cookie** or using **localStorage**. -From this point, you should pass this token to every API call. +From this point, you should pass this token with every API call. -Sample call using the Authorization header using AngularJS: +#### Sample Call Using The Authorization Header With AngularJS ```javascript -app.config( function( $httpProvider ) { - $httpProvider.interceptors.push( [ '$q', '$location', '$cookies', function( $q, $location, $cookies ) { - return { - 'request': function( config ) { - config.headers = config.headers || {}; - //Assume that you store the token in a cookie. - var globals = $cookies.getObject( 'globals' ) || {}; - //If the cookie has the CurrentUser and the token - //add the Authorization header in each request - if ( globals.currentUser && globals.currentUser.token ) { - config.headers.Authorization = 'Bearer ' + globals.currentUser.token; - } - return config; +app.config(function ($httpProvider) { + $httpProvider.interceptors.push([ + '$q', + '$location', + '$cookies', + function ($q, $location, $cookies) { + return { + request: function (config) { + config.headers = config.headers || {} + // Assume that you store the token in a cookie + var globals = $cookies.getObject('globals') || {} + // If the cookie has the CurrentUser and the token + // add the Authorization header in each request + if (globals.currentUser && globals.currentUser.token) { + config.headers.Authorization = 'Bearer ' + globals.currentUser.token + } + return config + }, } - }; - } ] ); -} ); + }, + ]) +}) ``` -The **wp-api-jwt-auth** will intercept every call to the server and will look for the authorization header, if the authorization header is present, it will try to decode the token and will set the user according with the data stored in it. +The **wp-api-jwt-auth** plugin will intercept every call to the server and will look for the Authorization Header. If the Authorization header is present, it will try to decode the token and will set the user according to the data stored in it. -If the token is valid, the API call flow will continue as always. +If the token is valid, the API call flow will continue as normal. **Sample Headers** -``` +```http POST /resource HTTP/1.1 Host: server.example.com Authorization: Bearer mF_s9.B5f-4.1JqM ``` -### Errors +## Errors -If the token is invalid an error will be returned. Here are some samples of errors: +If the token is invalid, an error will be returned. Here are some sample errors: **Invalid Credentials** @@ -234,9 +283,11 @@ If the token is invalid an error will be returned. Here are some samples of erro ] ``` +**Need advanced error tracking?** [JWT Authentication PRO](https://jwtauth.pro/?utm_source=github_readme&utm_medium=link&utm_campaign=pro_promotion&utm_content=errors_pro_note) offers enhanced error tracking and monitoring capabilities. + ### /wp-json/jwt-auth/v1/token/validate -This is a simple helper endpoint to validate a token; you only will need to make a POST request sending the Authorization header. +This is a simple helper endpoint to validate a token. You only need to make a POST request with the Authorization header. Valid Token Response: @@ -251,11 +302,11 @@ Valid Token Response: ## Available Hooks -The **wp-api-jwt-auth** is dev friendly and has five filters available to override the default settings. +The **wp-api-jwt-auth** plugin is developer-friendly and provides five filters to override the default settings. -#### jwt_auth_cors_allow_headers +### jwt_auth_cors_allow_headers -The **jwt_auth_cors_allow_headers** allows you to modify the available headers when the CORs support is enabled. +The **jwt_auth_cors_allow_headers** filter allows you to modify the available headers when CORS support is enabled. Default Value: @@ -265,7 +316,7 @@ Default Value: ### jwt_auth_not_before -The **jwt_auth_not_before** allows you to change the [**nbf**](https://tools.ietf.org/html/rfc7519#section-4.1.5) value before the token is created. +The **jwt_auth_not_before** filter allows you to change the [**nbf**](https://tools.ietf.org/html/rfc7519#section-4.1.5) value before the token is created. Default Value: @@ -275,7 +326,7 @@ Creation time - time() ### jwt_auth_expire -The **jwt_auth_expire** allows you to change the value [**exp**](https://tools.ietf.org/html/rfc7519#section-4.1.4) before the token is created. +The **jwt_auth_expire** filter allows you to change the [**exp**](https://tools.ietf.org/html/rfc7519#section-4.1.4) value before the token is created. Default Value: @@ -285,7 +336,7 @@ time() + (DAY_IN_SECONDS * 7) ### jwt_auth_token_before_sign -The **jwt_auth_token_before_sign** allows you to modify all the token data before to be encoded and signed. +The **jwt_auth_token_before_sign** filter allows you to modify all token data before it is encoded and signed. Default value: @@ -304,8 +355,11 @@ $token = array( ); ``` +**Want easier customization?** [JWT Authentication PRO](https://jwtauth.pro/?utm_source=github_readme&utm_medium=link&utm_campaign=pro_promotion&utm_content=hook_payload_pro_note) allows you to add custom claims directly through the admin UI. + ### jwt_auth_token_before_dispatch -The **jwt_auth_token_before_dispatch** allows you to modify all the response array before to dispatch it to the client. + +The **jwt_auth_token_before_dispatch** filter allows you to modify the response array before it is sent to the client. Default value: @@ -319,25 +373,66 @@ $data = array( ); ``` -## Testing +### jwt_auth_algorithm + +The **jwt_auth_algorithm** filter allows you to modify the signing algorithm. -Since version **1.1.0** I've added a new test suite to be sure that the basic features of this plugin do what it's expected. +Default value: + +```php + + * + * @since 1.3.4 + */ +class Jwt_Auth_Admin +{ + /** + * The ID of this plugin. + * + * @since 1.3.4 + * + * @var string The ID of this plugin. + */ + private string $plugin_name; + + /** + * The version of this plugin. + * + * @since 1.3.4 + * + * @var string The current version of this plugin. + */ + private string $version; + + /** + * Initialize the class and set its properties. + * + * @param string $plugin_name The name of the plugin. + * @param string $version The version of this plugin. + * + * @since 1.3.4 + */ + public function __construct(string $plugin_name, string $version) + { + $this->plugin_name = $plugin_name; + $this->version = $version; + } + + /** + * Register admin REST API endpoints. + * + * @return void + * + * @since 1.3.4 + */ + public function register_admin_rest_routes() + { + $namespace = 'jwt-auth/v1'; + + register_rest_route( + $namespace, + 'admin/settings', + [ + 'methods' => ['GET', 'POST'], + 'callback' => [$this, 'handle_settings'], + 'permission_callback' => [$this, 'settings_permission_check'], + ] + ); + + register_rest_route( + $namespace, + 'admin/status', + [ + 'methods' => 'GET', + 'callback' => [$this, 'get_configuration_status'], + 'permission_callback' => [$this, 'settings_permission_check'], + ] + ); + + register_rest_route( + $namespace, + 'admin/survey', + [ + 'methods' => 'POST', + 'callback' => [$this, 'handle_survey_submission'], + 'permission_callback' => [$this, 'settings_permission_check'], + ] + ); + + register_rest_route( + $namespace, + 'admin/survey/status', + [ + 'methods' => 'GET', + 'callback' => [$this, 'get_survey_status'], + 'permission_callback' => [$this, 'settings_permission_check'], + ] + ); + + register_rest_route( + $namespace, + 'admin/survey/complete', + [ + 'methods' => 'POST', + 'callback' => [$this, 'mark_survey_completed'], + 'permission_callback' => [$this, 'settings_permission_check'], + ] + ); + + register_rest_route( + $namespace, + 'admin/survey/dismissal', + [ + 'methods' => ['GET', 'POST'], + 'callback' => [$this, 'handle_survey_dismissal'], + 'permission_callback' => [$this, 'settings_permission_check'], + ] + ); + + register_rest_route( + $namespace, + 'admin/dashboard', + [ + 'methods' => 'GET', + 'callback' => [$this, 'get_dashboard_data'], + 'permission_callback' => [$this, 'settings_permission_check'], + ] + ); + + register_rest_route( + $namespace, + 'admin/notices/dismiss', + [ + 'methods' => 'POST', + 'callback' => [$this, 'handle_notice_dismissal'], + 'permission_callback' => [$this, 'settings_permission_check'], + 'args' => [ + 'notice_id' => [ + 'required' => true, + 'type' => 'string', + 'description' => 'The ID of the notice to dismiss', + 'validate_callback' => function ($param) { + return is_string($param) && ! empty($param); + }, + ], + ], + ] + ); + } + + /** + * Check permissions for settings endpoint. + * + * @return bool + */ + public function settings_permission_check() + { + return current_user_can('manage_options'); + } + + /** + * Handle settings GET and POST requests. + * + * @return WP_REST_Response|WP_Error + */ + public function handle_settings(WP_REST_Request $request) + { + if ($request->get_method() === 'GET') { + $settings = get_option( + 'jwt_auth_options', + [ + 'share_data' => false, + ] + ); + + return new WP_REST_Response( + [ + 'jwt_auth_options' => $settings, + ], + 200 + ); + } + + if ($request->get_method() === 'POST') { + $settings = $request->get_param('jwt_auth_options'); + + if (! $settings || ! is_array($settings)) { + return new WP_Error( + 'jwt_auth_invalid_settings', + 'Invalid settings data provided.', + ['status' => 400] + ); + } + + // Sanitize and validate settings + $sanitized_settings = []; + + if (isset($settings['share_data'])) { + $sanitized_settings['share_data'] = (bool) $settings['share_data']; + } + + // Merge with existing settings + $existing_settings = get_option('jwt_auth_options', []); + $updated_settings = array_merge($existing_settings, $sanitized_settings); + + $success = update_option('jwt_auth_options', $updated_settings); + + // update_option returns false if the value hasn't changed, so we need to check differently + if ($success === false && get_option('jwt_auth_options') !== $updated_settings) { + return new WP_Error( + 'jwt_auth_settings_update_failed', + 'Failed to update settings.', + ['status' => 500] + ); + } + + return new WP_REST_Response( + [ + 'jwt_auth_options' => $updated_settings, + ], + 200 + ); + } + + return new WP_Error( + 'jwt_auth_method_not_allowed', + 'Method not allowed.', + ['status' => 405] + ); + } + + /** + * Get real configuration status for the dashboard. + * + * @return WP_REST_Response + */ + public function get_configuration_status() + { + $secret_key = defined('JWT_AUTH_SECRET_KEY') ? JWT_AUTH_SECRET_KEY : false; + $cors_enabled = defined('JWT_AUTH_CORS_ENABLE') ? JWT_AUTH_CORS_ENABLE : false; + $dev_mode = defined('JWT_AUTH_DEV_MODE') ? JWT_AUTH_DEV_MODE : false; + + // Check if JWT secret key is configured + $secret_key_configured = ! empty($secret_key); + + // Check PHP version compatibility + $php_version = PHP_VERSION; + $php_compatible = version_compare($php_version, '7.4', '>='); + $pro_compatible = version_compare($php_version, '7.4', '>='); + + // WordPress version + $wp_version = get_bloginfo('version'); + + // MySQL version + global $wpdb; + $mysql_version = $wpdb->get_var('SELECT VERSION()') ?: 'Unknown'; + + // PHP Memory Limit + $memory_limit = ini_get('memory_limit'); + + // PHP Post Max Size + $post_max_size = ini_get('post_max_size'); + + // Configuration method detection + $config_method = $secret_key_configured ? 'wp-config.php' : 'Not configured'; + + $status = [ + 'configuration' => [ + 'method' => $config_method, + 'secret_key_configured' => $secret_key_configured, + 'cors_enabled' => $cors_enabled, + 'dev_mode' => $dev_mode, + ], + 'system' => [ + 'php_version' => $php_version, + 'php_compatible' => $php_compatible, + 'pro_compatible' => $pro_compatible, + 'wordpress_version' => $wp_version, + 'mysql_version' => $mysql_version, + 'php_memory_limit' => $memory_limit, + 'post_max_size' => $post_max_size, + ], + ]; + + return new WP_REST_Response($status, 200); + } + + /** + * Register a new settings page under Settings main menu + * . + * + * @return void + * + * @since 1.3.4 + */ + public function register_menu_page() + { + add_submenu_page( + 'options-general.php', + __('JWT Authentication', 'jwt-auth'), + __('JWT Authentication', 'jwt-auth'), + 'manage_options', + 'jwt_authentication', + [$this, 'render_admin_page'] + ); + + // Add Token Dashboard submenu item + add_submenu_page( + 'options-general.php', + __('Token Dashboard', 'jwt-auth'), + __(' â†ŗ Token Details 👑', 'jwt-auth'), + 'manage_options', + 'jwt_token_dashboard', + [$this, 'render_token_dashboard_page'] + ); + } + + /** + * Admin notices system storage. + * + * @var array + * + * @since 1.3.8 + */ + private $admin_notices = []; + + /** + * Register a new admin notice to be displayed. + * + * @param string $id Unique notice identifier + * @param string $message Notice message + * @param string $type Notice type (success, error, warning, info) + * @param string $cta_text Call to action button text (optional) + * @param string $cta_link Call to action button link (optional) + * @param bool $dismissible Whether the notice is dismissible + * @return void + * + * @since 1.3.8 + */ + public function register_admin_notice($id, $message, $type = 'info', $cta_text = '', $cta_link = '', $dismissible = true) + { + $this->admin_notices[$id] = [ + 'id' => $id, + 'message' => $message, + 'type' => $type, + 'cta_text' => $cta_text, + 'cta_link' => $cta_link, + 'dismissible' => $dismissible, + ]; + } + + /** + * Display all admin notices (registers and displays them). + * + * @return void + * + * @since 1.3.8 + */ + public function display_all_notices() + { + // First register all notices + $this->register_system_notices(); + + // Check if we have notices to display + $has_notices = false; + foreach ($this->admin_notices as $notice) { + if ($this->should_display_notice($notice['id'])) { + $this->render_admin_notice($notice); + $has_notices = true; + } + } + + // Only enqueue dismissal script if we have notices + if ($has_notices) { + $this->enqueue_notice_dismissal_script(); + } + } + + /** + * Register system notices (configuration, welcome, etc.). + * + * @return void + * + * @since 1.3.8 + */ + private function register_system_notices() + { + // Welcome notice for new installations - show until dismissed or CTA clicked + $this->register_admin_notice( + 'jwt_auth_welcome', + __('JWT Authentication installed successfully! Configure your settings to enable REST API authentication.', 'jwt-auth'), + 'success', + __('Configure JWT Authentication →', 'jwt-auth'), + admin_url('http://github.com/options-general.php?page=jwt_authentication') + ); + } + + /** + * Check if a notice should be displayed. + * + * @param string $notice_id + * @return bool + * + * @since 1.3.8 + */ + private function should_display_notice($notice_id) + { + // Check if user has appropriate permissions + if (! current_user_can('manage_options')) { + return false; + } + + // Check if notice has been dismissed + $dismissed_notices = get_option('jwt_auth_dismissed_notices', []); + if (in_array($notice_id, $dismissed_notices, true)) { + return false; + } + + return true; + } + + /** + * Render a single admin notice. + * + * @param array $notice Notice configuration + * @return void + * + * @since 1.3.8 + */ + private function render_admin_notice($notice) + { + $notice_class = 'notice notice-'.esc_attr($notice['type']); + if ($notice['dismissible']) { + $notice_class .= ' is-dismissible'; + } + ?> +
+

+ + + + + + +

+
+ register_admin_notice($id, $message, 'success', $cta_text, $cta_link); + } + + /** + * Display a generic info notice. + * + * @param string $id Notice ID + * @param string $message Notice message + * @param string $cta_text CTA button text + * @param string $cta_link CTA button link + * @return void + * + * @since 1.3.8 + */ + public function display_info_notice($id, $message, $cta_text = '', $cta_link = '') + { + $this->register_admin_notice($id, $message, 'info', $cta_text, $cta_link); + } + + /** + * Display a generic warning notice. + * + * @param string $id Notice ID + * @param string $message Notice message + * @param string $cta_text CTA button text + * @param string $cta_link CTA button link + * @return void + * + * @since 1.3.8 + */ + public function display_warning_notice($id, $message, $cta_text = '', $cta_link = '') + { + $this->register_admin_notice($id, $message, 'warning', $cta_text, $cta_link); + } + + /** + * Display a generic error notice. + * + * @param string $id Notice ID + * @param string $message Notice message + * @param string $cta_text CTA button text + * @param string $cta_link CTA button link + * @return void + * + * @since 1.3.8 + */ + public function display_error_notice($id, $message, $cta_text = '', $cta_link = '') + { + $this->register_admin_notice($id, $message, 'error', $cta_text, $cta_link); + } + + /** + * Enqueue the plugin assets only on the plugin settings page. + * + * @param string $suffix + * @return void|null + * + * @since 1.3.4 + */ + public function enqueue_plugin_assets($suffix = '') + { + // Check if $suffix is empty or null + if (empty($suffix)) { + return; // Exit early to prevent further execution + } + + if ($suffix !== 'settings_page_jwt_authentication' && $suffix !== 'settings_page_jwt_token_dashboard') { + return null; + } + + $is_dev_mode = defined('JWT_AUTH_DEV_MODE') && JWT_AUTH_DEV_MODE; + + if ($is_dev_mode) { + // Development mode - set up React Refresh preamble first + add_action( + 'admin_head', + function () { + echo ''; + } + ); + + // Load Vite client + wp_enqueue_script( + 'vite-client', + 'http://localhost:5173/@vite/client', + [], + null, + true + ); + + // Load our main app + wp_enqueue_script( + $this->plugin_name.'-settings', + 'http://localhost:5173/admin/ui/src/main.tsx', + ['vite-client'], + null, + true + ); + + // Add type="module" to the scripts + add_filter( + 'script_loader_tag', + function ($tag, $handle) { + if (in_array($handle, ['vite-client', $this->plugin_name.'-settings'])) { + return str_replace('plugin_name.'-settings', + plugins_url('http://github.com/ui/dist/main.js',%20__FILE__), + [], + $this->version, + ['in_footer' => true] + ); + + wp_enqueue_style( + $this->plugin_name.'-settings', + plugins_url('http://github.com/ui/dist/main.css',%20__FILE__), + [], + $this->version + ); + } + + // Provide WordPress API configuration to React app + if ($is_dev_mode) { + // For dev mode, we need to add the config manually since we're not using wp_enqueue_script + add_action( + 'admin_footer', + function () { + $config = [ + 'apiUrl' => rest_url('http://github.com/jwt-auth/v1/admin/settings'), + 'nonce' => wp_create_nonce('wp_rest'), + 'siteUrl' => get_bloginfo('url'), + 'settings' => get_option('jwt_auth_options', ['share_data' => false]), + 'siteProfile' => [ + 'phpVersion' => PHP_VERSION, + 'wordpressVersion' => get_bloginfo('version'), + 'isProCompatible' => version_compare(PHP_VERSION, '7.4', '>='), + 'isWooCommerceDetected' => class_exists('WooCommerce'), + 'pluginCount' => count(get_option('active_plugins', [])), + 'signingAlgorithm' => 'HS256', + ], + ]; + echo ''; + }, + 5 + ); // Priority 5 to run before the module script + } else { + wp_localize_script( + $this->plugin_name.'-settings', + 'jwtAuthConfig', + [ + 'apiUrl' => rest_url('http://github.com/jwt-auth/v1/admin/settings'), + 'nonce' => wp_create_nonce('wp_rest'), + 'siteUrl' => get_bloginfo('url'), + 'settings' => get_option('jwt_auth_options', ['share_data' => false]), + 'siteProfile' => [ + 'phpVersion' => PHP_VERSION, + 'wordpressVersion' => get_bloginfo('version'), + 'isProCompatible' => version_compare(PHP_VERSION, '7.4', '>='), + 'isWooCommerceDetected' => class_exists('WooCommerce'), + 'pluginCount' => count(get_option('active_plugins', [])), + 'signingAlgorithm' => 'HS256', + ], + ] + ); + } + } + + /** + * Enqueue notice dismissal JavaScript. + * + * @return void + * + * @since 1.3.8 + */ + private function enqueue_notice_dismissal_script() + { + if (! is_admin()) { + return; + } + + $script = " + document.addEventListener('DOMContentLoaded', function() { + // Function to dismiss a notice via AJAX + function dismissNotice(noticeId, callback) { + if (!noticeId) { + if (callback) callback(); + return; + } + + const formData = new FormData(); + formData.append('notice_id', noticeId); + + fetch('".rest_url('http://github.com/jwt-auth/v1/admin/notices/dismiss')."', { + method: 'POST', + headers: { + 'X-WP-Nonce': '".wp_create_nonce('wp_rest')."' + }, + body: formData + }) + .then(response => response.json()) + .then(data => { + console.log('JWT Auth: Notice dismissed successfully', data); + if (callback) callback(); + }) + .catch(error => { + console.error('JWT Auth: Failed to dismiss notice', error); + if (callback) callback(); + }); + } + + // Handle dismiss button (X) clicks - only for JWT Auth notices + document.addEventListener('click', function(e) { + if (e.target.matches('.notice[data-notice-id^=\"jwt_auth_\"] .notice-dismiss')) { + const notice = e.target.closest('.notice[data-notice-id^=\"jwt_auth_\"]'); + const noticeId = notice ? notice.dataset.noticeId : null; + dismissNotice(noticeId, function() { + // Reload page to let backend handle notice visibility + window.location.reload(); + }); + } + }); + + // Handle CTA button clicks - only for JWT Auth notices + document.addEventListener('click', function(e) { + if (e.target.matches('.notice[data-notice-id^=\"jwt_auth_\"] .button')) { + const notice = e.target.closest('.notice[data-notice-id^=\"jwt_auth_\"]'); + const noticeId = notice ? notice.dataset.noticeId : null; + const href = e.target.getAttribute('href'); + + if (noticeId && href) { + e.preventDefault(); + + // Dismiss notice first, then navigate + dismissNotice(noticeId, function() { + window.location.href = href; + }); + } + } + }); + }); + "; + + wp_add_inline_script('wp-util', $script); + } + + /** + * Register the plugin settings. + * + * @return void + * + * @since 1.3.4 + */ + public function register_plugin_settings() + { + register_setting( + 'jwt_auth', + 'jwt_auth_options', + [ + 'type' => 'object', + 'default' => [ + 'share_data' => false, + ], + 'show_in_rest' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'share_data' => [ + 'type' => 'boolean', + 'default' => false, + ], + ], + ], + ], + ] + ); + } + + /** + * Render the plugin settings page. + * This is a React application that will be rendered on the admin page. + * + * @return void + * + * @since 1.3.4 + */ + public function render_admin_page() + { + ?> +
+ +
+ 'Add Token Dashboard', + 'utm_content' => 'token-dashboard-primary', + ]; + + $base_pro_url = 'https://jwtauth.pro/upgrade'; + $utm_params = [ + 'utm_source' => 'plugin-list', + 'utm_medium' => 'action-link', + 'utm_campaign' => 'feature-highlight', + 'utm_content' => $selected_variation['utm_content'], + ]; + + $pro_link_url = (string) add_query_arg($utm_params, $base_pro_url); + $pro_link_style = 'style="color: #00a32a; font-weight: 700; text-decoration: none;" onmouseover="this.style.color=\'#008a20\';" onmouseout="this.style.color=\'#00a32a\';"'; + + $pro_link_text = $selected_variation['text']; + $links[] = ''.$pro_link_text.''; + } + + return $links; + } + + /** + * Handle survey submission. + * + * @return WP_REST_Response|WP_Error + */ + public function handle_survey_submission(WP_REST_Request $request) + { + $survey_data = $request->get_json_params(); + + if (! $survey_data) { + return new WP_Error( + 'jwt_auth_invalid_survey_data', + 'Invalid survey data provided.', + ['status' => 400] + ); + } + + // Sanitize survey data + $sanitized_data = [ + 'useCase' => sanitize_text_field($survey_data['useCase'] ?? ''), + 'useCaseOther' => sanitize_textarea_field($survey_data['useCaseOther'] ?? ''), + 'projectTimeline' => sanitize_text_field($survey_data['projectTimeline'] ?? ''), + 'primaryChallenge' => sanitize_text_field($survey_data['primaryChallenge'] ?? ''), + 'primaryChallengeOther' => sanitize_textarea_field($survey_data['primaryChallengeOther'] ?? ''), + 'purchaseInterest' => sanitize_text_field($survey_data['purchaseInterest'] ?? ''), + 'email' => sanitize_email($survey_data['email'] ?? ''), + 'emailConsent' => (bool) ($survey_data['emailConsent'] ?? false), + 'submittedAt' => sanitize_text_field($survey_data['submittedAt'] ?? ''), + ]; + + // Send to webhook (non-blocking) + $webhook_url = apply_filters('jwt_auth_survey_webhook_url', Jwt_Auth::REMOTE_API_URL.'/api/survey'); + $this->send_survey_to_webhook($sanitized_data, $webhook_url); + + return new WP_REST_Response( + [ + 'success' => true, + 'message' => 'Survey submitted successfully.', + ], + 200 + ); + } + + /** + * Get survey completion status for current user. + * + * @return WP_REST_Response + */ + public function get_survey_status() + { + $user_id = get_current_user_id(); + $completed_at = get_user_meta($user_id, 'jwt_auth_survey_completed', true); + + return new WP_REST_Response( + [ + 'completed' => ! empty($completed_at), + 'completedAt' => $completed_at ?: null, + ], + 200 + ); + } + + /** + * Mark survey as completed for current user. + * + * @return WP_REST_Response + */ + public function mark_survey_completed(WP_REST_Request $request) + { + $user_id = get_current_user_id(); + $completed_at = $request->get_param('completedAt') ?: current_time('mysql'); + + $success = update_user_meta($user_id, 'jwt_auth_survey_completed', $completed_at); + + if (! $success) { + return new WP_Error( + 'jwt_auth_survey_completion_failed', + 'Failed to mark survey as completed.', + ['status' => 500] + ); + } + + return new WP_REST_Response( + [ + 'success' => true, + 'completedAt' => $completed_at, + ], + 200 + ); + } + + /** + * Handle survey floating card dismissal tracking. + * + * @return WP_REST_Response|WP_Error + */ + public function handle_survey_dismissal(WP_REST_Request $request) + { + $user_id = get_current_user_id(); + + if ($request->get_method() === 'GET') { + // Get dismissal data + $dismissal_data = get_user_meta($user_id, 'jwt_auth_survey_dismissal', true); + + if (! $dismissal_data) { + $dismissal_data = [ + 'count' => 0, + 'lastDismissedAt' => null, + 'hideUntil' => null, + ]; + } + + // Check if we should show the card + $now = time(); + $shouldShow = true; + + if ($dismissal_data['count'] >= 3) { + $shouldShow = false; + } elseif ($dismissal_data['hideUntil'] && $now < $dismissal_data['hideUntil']) { + $shouldShow = false; + } + + return new WP_REST_Response( + [ + 'dismissalCount' => $dismissal_data['count'], + 'lastDismissedAt' => $dismissal_data['lastDismissedAt'], + 'shouldShow' => $shouldShow, + ], + 200 + ); + } + + if ($request->get_method() === 'POST') { + // Update dismissal data + $dismissal_data = get_user_meta($user_id, 'jwt_auth_survey_dismissal', true) ?: [ + 'count' => 0, + 'lastDismissedAt' => null, + 'hideUntil' => null, + ]; + + $dismissal_data['count']++; + $dismissal_data['lastDismissedAt'] = current_time('mysql'); + + // Hide for 14 days if not already at max dismissals + if ($dismissal_data['count'] < 3) { + $dismissal_data['hideUntil'] = time() + (14 * DAY_IN_SECONDS); + } + + $success = update_user_meta($user_id, 'jwt_auth_survey_dismissal', $dismissal_data); + + if (! $success) { + return new WP_Error( + 'jwt_auth_dismissal_update_failed', + 'Failed to update dismissal data.', + ['status' => 500] + ); + } + + return new WP_REST_Response( + [ + 'success' => true, + 'dismissalCount' => $dismissal_data['count'], + 'shouldShow' => $dismissal_data['count'] < 4, + ], + 200 + ); + } + + return new WP_Error( + 'jwt_auth_method_not_allowed', + 'Method not allowed.', + ['status' => 405] + ); + } + + /** + * Send survey data to webhook (non-blocking). + * + * @param array $survey_data + * @param string $webhook_url + * @return void + */ + private function send_survey_to_webhook($survey_data, $webhook_url) + { + wp_remote_post( + $webhook_url, + [ + 'timeout' => 5, + 'blocking' => false, + // TODO: remove this once we have a valid SSL certificate + 'sslverify' => false, + 'headers' => [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'JWT-Auth-Plugin/'.$this->version, + ], + 'body' => wp_json_encode($survey_data), + ] + ); + } + + /** + * Get consolidated dashboard data. + * + * @param WP_REST_Request $request The REST request object. + * @return WP_REST_Response|WP_Error + */ + public function get_dashboard_data($request) + { + // $request parameter is required for REST API compatibility but not used in this method + unset($request); + + try { + // Get settings data + $settings_request = new WP_REST_Request('GET', '/jwt-auth/v1/admin/settings'); + $settings_response = $this->handle_settings($settings_request); + + if (is_wp_error($settings_response)) { + return $settings_response; + } + + $settings_data = $settings_response->get_data(); + + // Get configuration status + $status_response = $this->get_configuration_status(); + + if (is_wp_error($status_response)) { + return $status_response; + } + + $status_data = $status_response->get_data(); + + // Get survey status + $survey_status_response = $this->get_survey_status(); + + if (is_wp_error($survey_status_response)) { + return $survey_status_response; + } + + $survey_status_data = $survey_status_response->get_data(); + + // Get survey dismissal status + $dismissal_request = new WP_REST_Request('GET', '/jwt-auth/v1/admin/survey/dismissal'); + $dismissal_response = $this->handle_survey_dismissal($dismissal_request); + + if (is_wp_error($dismissal_response)) { + return $dismissal_response; + } + + $dismissal_data = $dismissal_response->get_data(); + + // Return consolidated data + return new WP_REST_Response( + [ + 'settings' => $settings_data['jwt_auth_options'], + 'jwtStatus' => $status_data, + 'surveyStatus' => $survey_status_data, + 'surveyDismissal' => $dismissal_data, + ], + 200 + ); + } catch (Exception $e) { + return new WP_Error( + 'jwt_auth_dashboard_error', + 'Failed to retrieve dashboard data: '.$e->getMessage(), + ['status' => 500] + ); + } + } + + /** + * Handle admin notice dismissal via REST API. + * + * @return WP_REST_Response|WP_Error + * + * @since 1.3.8 + */ + public function handle_notice_dismissal(WP_REST_Request $request) + { + $notice_id = $request->get_param('notice_id'); + + if (empty($notice_id)) { + return new WP_Error( + 'jwt_auth_missing_notice_id', + 'Notice ID is required.', + ['status' => 400] + ); + } + + $notice_id = sanitize_text_field($notice_id); + + $success = $this->dismiss_notice($notice_id); + + if (! $success) { + return new WP_Error( + 'jwt_auth_notice_dismissal_failed', + 'Failed to dismiss notice.', + ['status' => 500] + ); + } + + return new WP_REST_Response( + [ + 'success' => true, + 'notice_id' => $notice_id, + 'message' => 'Notice dismissed successfully.', + ], + 200 + ); + } +} diff --git a/admin/class-jwt-auth-cron.php b/admin/class-jwt-auth-cron.php new file mode 100644 index 00000000..052602a3 --- /dev/null +++ b/admin/class-jwt-auth-cron.php @@ -0,0 +1,101 @@ + + * @since 1.3.4 + */ +class Jwt_Auth_Cron +{ + + /** + * If the user agrees to share data, then we will send some data. + * + * @return void|null + * @since 1.3.4 + */ + static public function collect() { + // if the user doesn't agree to share data, then we don't do anything + if ( ! self::allow_shared_data() ) { + return null; + } + + // get the PHP version + $php_version = phpversion(); + // get the WP version + $wp_version = get_bloginfo( 'version' ); + // get the list of activated plugins + $active_plugins = get_option( 'active_plugins' ); + // get the number of activated plugins + $plugins_count = count( $active_plugins ); + // is WooCommerce installed and activated? + $woocommerce_installed = in_array( 'woocommerce/woocommerce.php', $active_plugins ); + // get the WooCommerce version + $woocommerce_version = $woocommerce_installed ? get_option( 'woocommerce_version' ) : null; + // get the site URL and hash it (we don't want to store the URL in plain text) + $site_url_hash = hash( 'sha256', get_site_url() ); + + $data = [ + 'php_version' => $php_version, + 'wp_version' => $wp_version, + 'plugins_count' => $plugins_count, + 'woocommerce_version' => $woocommerce_version + ]; + + // Wrap the request in a try/catch to avoid fatal errors + // and set the timeout to 5 seconds to avoid long delays + try { + $api_url = Jwt_Auth::REMOTE_API_URL . '/api/collect'; + $response = wp_remote_post($api_url . '/' . $site_url_hash, [ + 'body' => $data, + 'timeout' => 5, + // TODO: remove this once we have a valid SSL certificate + 'sslverify' => false, + ] ); + + error_log('Jwt_Auth_Cron::collect response'); + error_log(print_r($response, true)); + } catch (Exception $e) { + error_log( 'Error adding site to remote database' ); + error_log( $e->getMessage() ); + } + } + + /** + * If the user agrees to share data, then we will remove the site from the remote database. + * + * @return void|null + * @since 1.3.4 + */ + static public function remove() { + // First we remove the scheduled event + wp_clear_scheduled_hook( 'jwt_auth_share_data' ); + // Then we remove the site from the remote database + $site_url_hash = hash( 'sha256', get_site_url() ); + // Wrap the request in a try/catch to avoid fatal errors + // and set the timeout to 5 seconds to avoid long delays + try { + $api_url = Jwt_Auth::REMOTE_API_URL . '/api/destroy'; + wp_remote_post( $api_url . '/' . $site_url_hash, [ + 'timeout' => 5, + // TODO: remove this once we have a valid SSL certificate + 'sslverify' => false, + ] ); + } catch ( Exception $e ) { + error_log( 'Error removing site from remote database' ); + error_log( $e->getMessage() ); + } + } + + /** + * Check if the user agrees to share data. + * @return bool + * @since 1.3.4 + */ + static public function allow_shared_data(): bool { + $jwt_auth_options = get_option( 'jwt_auth_options' ); + + return ( isset( $jwt_auth_options['share_data'] ) && $jwt_auth_options['share_data'] ); + } +} diff --git a/admin/ui/src/App.tsx b/admin/ui/src/App.tsx new file mode 100644 index 00000000..2f23ab6f --- /dev/null +++ b/admin/ui/src/App.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import Dashboard from '@/components/Dashboard' +import '@/styles/globals.css' + +const App: React.FC = () => { + return +} + +export default App diff --git a/admin/ui/src/components/Dashboard.tsx b/admin/ui/src/components/Dashboard.tsx new file mode 100644 index 00000000..d8e2a230 --- /dev/null +++ b/admin/ui/src/components/Dashboard.tsx @@ -0,0 +1,203 @@ +import { useState, useEffect } from 'react' +import { Topbar } from './dashboard/topbar' +import { SurveyPage } from './survey/SurveyPage' +import { TokenDashboard } from './dashboard/token-dashboard' +import { AuthenticationStatusOverview } from './dashboard/authentication-status-overview' +import { ConfigurationHealthCheck } from './dashboard/configuration-health-check' +import { SystemEnvironment } from './dashboard/system-environment' +import { LiveApiExplorer } from './dashboard/live-api-explorer' +import { SetupConfiguration } from './dashboard/setup-configuration' +import { HelpImprove } from './dashboard/help-improve' +import { FloatingSurveyCTA } from './dashboard/floating-survey-cta' +import { wordpressAPI, type ConfigurationStatus } from '@/lib/wordpress-api' + +export default function Dashboard() { + const [shareData, setShareData] = useState(false) + const [configStatus, setConfigStatus] = useState(null) + const [isSurveyCtaVisible, setIsSurveyCtaVisible] = useState(false) + const [shouldShowSurveyCta, setShouldShowSurveyCta] = useState(false) + const [surveyCompleted, setSurveyCompleted] = useState(false) + const [isLoadingDismissal, setIsLoadingDismissal] = useState(false) + + // Initialize page based on URL hash, data attribute, or default to overview + const getInitialPage = (): 'overview' | 'survey' | 'token-dashboard' => { + // Check if initial page is set via data attribute (for direct page access) + const container = document.getElementById('jwt-auth-holder') + const initialPageFromAttribute = container?.getAttribute('data-initial-page') + + if (initialPageFromAttribute === 'token-dashboard') { + return 'token-dashboard' + } + + // Check URL hash and params for navigation within the app + const hash = window.location.hash.substring(1) + const params = new URLSearchParams(window.location.search) + + if (hash === 'survey' || params.get('page') === 'survey') { + return 'survey' + } + if (hash === 'token-dashboard' || params.get('page') === 'token-dashboard') { + return 'token-dashboard' + } + return 'overview' + } + + const [currentPage, setCurrentPage] = useState<'overview' | 'survey' | 'token-dashboard'>( + getInitialPage + ) + + // Load settings from WordPress on mount + useEffect(() => { + let isCancelled = false + + async function loadData() { + try { + // Load all dashboard data in a single API call + const dashboardData = await wordpressAPI.getDashboardData() + + // Only update state if component is still mounted + if (!isCancelled) { + setShareData(dashboardData.settings.share_data ?? false) + setConfigStatus(dashboardData.jwtStatus) + setSurveyCompleted(dashboardData.surveyStatus.completed ?? false) + setShouldShowSurveyCta( + (dashboardData.surveyDismissal.shouldShow ?? false) && + !(dashboardData.surveyStatus.completed ?? false) + ) + } + } catch (error) { + if (!isCancelled) { + console.error('Failed to load dashboard data:', error) + } + } + } + + loadData() + + // Cleanup function to cancel the effect if component unmounts + return () => { + isCancelled = true + } + }, []) + + // Listen for hash changes to support back/forward navigation + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.substring(1) + if (hash === 'survey' && currentPage !== 'survey') { + setCurrentPage('survey') + } else if (hash === 'token-dashboard' && currentPage !== 'token-dashboard') { + setCurrentPage('token-dashboard') + } else if (hash === 'overview' && currentPage !== 'overview') { + setCurrentPage('overview') + } else if (!hash && currentPage !== 'overview') { + setCurrentPage('overview') + } + } + + window.addEventListener('hashchange', handleHashChange) + return () => window.removeEventListener('hashchange', handleHashChange) + }, [currentPage]) + + // Handle scroll to show/hide survey CTA at 50% scroll + useEffect(() => { + const handleScroll = () => { + const scrollTop = window.scrollY + const documentHeight = document.documentElement.scrollHeight - window.innerHeight + const scrollPercent = (scrollTop / documentHeight) * 100 + // Don't show if user shouldn't see it, already completed survey, or if on survey/token-dashboard page + if ( + !shouldShowSurveyCta || + surveyCompleted || + currentPage === 'survey' || + currentPage === 'token-dashboard' + ) + return + + // Show/hide survey CTA based on scroll position + if (scrollPercent >= 50 && !isSurveyCtaVisible) { + setIsSurveyCtaVisible(true) + } else if (scrollPercent < 50 && isSurveyCtaVisible) { + setIsSurveyCtaVisible(false) + } + } + + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, [isSurveyCtaVisible, shouldShowSurveyCta, surveyCompleted, currentPage]) + + // Handle page navigation with URL updates + const handlePageChange = (page: 'overview' | 'survey' | 'token-dashboard') => { + setCurrentPage(page) + + // Update URL hash for deep linking + if (page === 'survey') { + window.history.pushState(null, '', '#survey') + } else if (page === 'token-dashboard') { + window.history.pushState(null, '', '#token-dashboard') + } else { + window.history.pushState(null, '', '#overview') + } + } + + const renderPage = () => { + switch (currentPage) { + case 'survey': + return ( + handlePageChange('overview')} + surveyCompleted={surveyCompleted} + /> + ) + case 'token-dashboard': + return handlePageChange('overview')} /> + case 'overview': + default: { + const isJwtConfigured = configStatus?.configuration?.secret_key_configured ?? false + + return ( +
+ +
+ + {isJwtConfigured ? ( + + ) : ( + + )} +
+ {isJwtConfigured && } + +
+ ) + } + } + } + + return ( +
+ +
+ {renderPage()} +
+ { + setIsLoadingDismissal(true) + setIsSurveyCtaVisible(false) + const success = await wordpressAPI.updateSurveyDismissal() + if (success) { + setShouldShowSurveyCta(false) + } + setIsLoadingDismissal(false) + }} + onTakeSurvey={() => handlePageChange('survey')} + /> +
+ ) +} diff --git a/admin/ui/src/components/dashboard/authentication-status-overview.tsx b/admin/ui/src/components/dashboard/authentication-status-overview.tsx new file mode 100644 index 00000000..f54413eb --- /dev/null +++ b/admin/ui/src/components/dashboard/authentication-status-overview.tsx @@ -0,0 +1,9 @@ +export const AuthenticationStatusOverview = () => ( +
+

JWT Authentication for WP-API

+

+ Enabling stateless authentication for your WordPress REST API with industry-standard JWT + tokens. +

+
+) diff --git a/admin/ui/src/components/dashboard/configuration-health-check.tsx b/admin/ui/src/components/dashboard/configuration-health-check.tsx new file mode 100644 index 00000000..0ee047f2 --- /dev/null +++ b/admin/ui/src/components/dashboard/configuration-health-check.tsx @@ -0,0 +1,89 @@ +import { CheckCircle, Loader2, X, AlertTriangle } from 'lucide-react' +import { InfoCard } from '@/components/ui/info-card' +import { StatusRow } from '@/components/ui/status-row' +import { Check } from '@/components/ui/check-icon' +import type { ConfigurationStatus } from '@/lib/wordpress-api' + +interface ConfigurationHealthCheckProps { + configStatus: ConfigurationStatus | null +} + +export const ConfigurationHealthCheck = ({ configStatus }: ConfigurationHealthCheckProps) => { + if (!configStatus) { + return ( + +
+ +
+
+ ) + } + + const { configuration } = configStatus + const allConfigured = configuration.secret_key_configured + + return ( + + {allConfigured ? 'Ready' : 'Needs Attention'} + {allConfigured ? ( + + ) : ( + + )} + + } + > + + + {configuration.secret_key_configured ? 'Configured & Valid' : 'Not Configured'} + + {configuration.secret_key_configured ? ( + + ) : ( + + )} + + + {configuration.cors_enabled ? 'Enabled' : 'Disabled'} + {configuration.cors_enabled ? ( + + ) : ( + + )} + + + + {configuration.secret_key_configured ? 'Active' : 'Inactive'} + + {configuration.secret_key_configured ? ( + + ) : ( + + )} + + + RFC 7519 Compliant + {configuration.secret_key_configured ? ( + + ) : ( + + )} + + {configuration.secret_key_configured && ( + + + + )} + + ) +} diff --git a/admin/ui/src/components/dashboard/floating-survey-cta.tsx b/admin/ui/src/components/dashboard/floating-survey-cta.tsx new file mode 100644 index 00000000..cc6d4b51 --- /dev/null +++ b/admin/ui/src/components/dashboard/floating-survey-cta.tsx @@ -0,0 +1,63 @@ +import { useState, useEffect } from 'react' +import { X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' + +interface FloatingSurveyCTAProps { + isVisible: boolean + onClose: () => void + onTakeSurvey: () => void +} + +export const FloatingSurveyCTA = ({ isVisible, onClose, onTakeSurvey }: FloatingSurveyCTAProps) => { + const [shouldRender, setShouldRender] = useState(false) + + useEffect(() => { + if (isVisible) { + setShouldRender(true) + } else { + // Delay unmounting to allow exit animation + const timer = setTimeout(() => setShouldRender(false), 500) + return () => clearTimeout(timer) + } + }, [isVisible]) + + if (!shouldRender) return null + + return ( +
+ + + + + Get 15% Off Pro! + +

+ Take our 2-min survey to help us improve and claim your discount. +

+ +
+
+
+ ) +} diff --git a/admin/ui/src/components/dashboard/help-improve.tsx b/admin/ui/src/components/dashboard/help-improve.tsx new file mode 100644 index 00000000..fc605754 --- /dev/null +++ b/admin/ui/src/components/dashboard/help-improve.tsx @@ -0,0 +1,39 @@ +import { Switch } from '@/components/ui/switch' +import { InfoCard } from '@/components/ui/info-card' +import { wordpressAPI } from '@/lib/wordpress-api' + +interface HelpImproveProps { + shareData: boolean + setShareData: (value: boolean) => void +} + +export const HelpImprove = ({ shareData, setShareData }: HelpImproveProps) => { + const handleToggle = async (checked: boolean) => { + try { + // Update the setting in WordPress + await wordpressAPI.updateSettings({ share_data: checked }) + // Update local state + setShareData(checked) + } catch (error) { + console.error('Failed to update sharing setting:', error) + // Could add toast notification here for better UX + } + } + + return ( + +
+
+

+ Enable Anonymous Sharing +

+

+ Help me build better features for your setup. I only collect technical info (PHP/WP + versions, use case) to improve compatibility and prioritize development. +

+
+ +
+
+ ) +} diff --git a/admin/ui/src/components/dashboard/live-api-explorer.tsx b/admin/ui/src/components/dashboard/live-api-explorer.tsx new file mode 100644 index 00000000..e3403843 --- /dev/null +++ b/admin/ui/src/components/dashboard/live-api-explorer.tsx @@ -0,0 +1,420 @@ +import { useState } from 'react' +import { CheckCircle, Loader2, Copy, Send, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism' +import { CodeSnippetDisplay } from '@/components/ui/code-snippet-display' +import { getCodeSnippets } from '@/lib/api-code-snippets' + +interface ApiResponse { + error?: string + details?: Record + [key: string]: unknown +} + +export const LiveApiExplorer = () => { + const [endpoint, setEndpoint] = useState('/jwt-auth/v1/token') + const [username, setUsername] = useState('testuser') + const [password, setPassword] = useState('password') + const [token, setToken] = useState('your-jwt-token') + const [isLoading, setIsLoading] = useState(false) + const [responseCopied, setResponseCopied] = useState(false) + const [tokenAutoFilled, setTokenAutoFilled] = useState(false) + const [tokenResponse, setTokenResponse] = useState(null) + const [validateResponse, setValidateResponse] = useState(null) + + // Get WordPress site URL from config + const siteUrl = window.jwtAuthConfig?.siteUrl || 'https://yoursite.com/' + + const handleEndpointChange = (newEndpoint: string) => { + setEndpoint(newEndpoint) + // Clear token auto-fill notifications when switching endpoints + setTokenAutoFilled(false) + } + + // Get the current response based on selected endpoint + const getCurrentResponse = () => { + return endpoint === '/jwt-auth/v1/token' ? tokenResponse : validateResponse + } + + // Set the response for the current endpoint + const setCurrentResponse = (response: ApiResponse | null) => { + if (endpoint === '/jwt-auth/v1/token') { + setTokenResponse(response) + } else { + setValidateResponse(response) + } + } + + const handleSend = async () => { + setIsLoading(true) + setCurrentResponse(null) + + try { + const fullUrl = `${siteUrl}/wp-json${endpoint}` + + // Prepare request headers and body based on endpoint + const headers: Record = { + 'Content-Type': 'application/json', + } + let requestBody: object = {} + + if (endpoint === '/jwt-auth/v1/token') { + if (!username.trim() || !password.trim()) { + setCurrentResponse({ + error: 'Username and password are required for token generation', + }) + setIsLoading(false) + return + } + requestBody = { + username: username.trim(), + password: password.trim(), + } + } else if (endpoint === '/jwt-auth/v1/token/validate') { + if (!token.trim()) { + setCurrentResponse({ + error: 'Token is required for validation', + }) + setIsLoading(false) + return + } + // For validation endpoint, send token in Authorization header + headers['Authorization'] = `Bearer ${token.trim()}` + requestBody = {} + } + + // Make the actual API request + const response = await fetch(fullUrl, { + method: 'POST', + headers, + body: JSON.stringify(requestBody), + }) + + // Handle response + if (response.ok) { + const data = await response.json() + setCurrentResponse(data) + + // Auto-fill token for validation if this was a successful token request + if (endpoint === '/jwt-auth/v1/token' && data.token) { + setToken(data.token) + setTokenAutoFilled(true) + // Hide the notice after 5 seconds + setTimeout(() => setTokenAutoFilled(false), 5000) + } + } else { + // Try to get error message from response + try { + const errorData = await response.json() + setCurrentResponse({ + error: `HTTP ${response.status}: ${errorData.message || response.statusText}`, + details: errorData, + }) + } catch { + setCurrentResponse({ + error: `HTTP ${response.status}: ${response.statusText}`, + }) + } + } + } catch (error) { + setCurrentResponse({ + error: `Network error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }) + } finally { + setIsLoading(false) + } + } + + const snippets = getCodeSnippets(endpoint, siteUrl, username, password, token) + + return ( + + + + Live API Explorer + + + Test your JWT endpoints in real-time and get instant code snippets. + + + +
+
+ +
+
+ {endpoint} +
+ +
+ +
+
+
+

+ Server +

+
+ + +
+
+ +
+

+ Request Body +

+
+ {endpoint === '/jwt-auth/v1/token' ? ( + <> +
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ + ) : ( +
+ + {tokenAutoFilled && ( +
+
+
+ +
+
+

+ Token Auto-filled: The JWT + token from your successful request has been automatically added below. +

+
+
+
+ )} + { + setToken(e.target.value) + setTokenAutoFilled(false) // Hide notice when user manually edits + }} + /> +
+ )} +
+
+
+ +
+ + + cURL + PHP + JavaScript + Python + + + + + + + + + + + + + + + +
+

+ Response +

+ {/* Loading State */} + {isLoading && ( +
+
+
+ +
+
+

Sending request...

+
+
+
+ )} + {/* Success/Error Alert */} + {getCurrentResponse() && ( +
+ {getCurrentResponse()?.error ? ( +
+
+
+ +
+
+

+ Request Failed:{' '} + {String(getCurrentResponse()?.error)} +

+
+
+
+ ) : ( +
+
+
+ +
+
+

+ Request Successful: The API + request completed successfully + {endpoint === '/jwt-auth/v1/token' && + ' and the token is ready to be used on the validate endpoint'} + . +

+
+
+
+ )} +
+ )} + {/* Response Content Box */} + {getCurrentResponse() && ( +
+
+ + Response Body + +
+
+
+ + {JSON.stringify(getCurrentResponse(), null, 2)} + +
+ +
+
+ )} + {/* Empty State */} + {!isLoading && !getCurrentResponse() && ( +
+
+ +
+

+ Ready to test your API +

+

+ Click the "Send" button above to make a request +

+
+ )} +
+
+
+
+
+ ) +} diff --git a/admin/ui/src/components/dashboard/setup-configuration.tsx b/admin/ui/src/components/dashboard/setup-configuration.tsx new file mode 100644 index 00000000..f6cac132 --- /dev/null +++ b/admin/ui/src/components/dashboard/setup-configuration.tsx @@ -0,0 +1,145 @@ +import { useState, useMemo, useCallback } from 'react' +import { Copy, RefreshCw, Key, AlertTriangle, CheckCircle } from 'lucide-react' +import { InfoCard } from '@/components/ui/info-card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Switch } from '@/components/ui/switch' +import { Label } from '@/components/ui/label' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism' + +const CONFIG = { + KEY_LENGTH: 64, + COPY_FEEDBACK_DURATION: 2000, + GENERATION_DELAY: 800, + CHAR_SET: + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;:,.<>?', +} as const + +// Generate a secure random key similar to WordPress salt generator +const generateSecureKey = (): string => { + let result = '' + const array = new Uint8Array(CONFIG.KEY_LENGTH) + window.crypto.getRandomValues(array) + + for (let i = 0; i < CONFIG.KEY_LENGTH; i++) { + result += CONFIG.CHAR_SET[array[i] % CONFIG.CHAR_SET.length] + } + + return result +} + +export const SetupConfiguration = () => { + const [generatedKey, setGeneratedKey] = useState(() => generateSecureKey()) + const [isGenerating, setIsGenerating] = useState(false) + const [corsEnabled, setCorsEnabled] = useState(false) + const [copySuccess, setCopySuccess] = useState(false) + + const handleGenerateKey = useCallback(async () => { + setIsGenerating(true) + await new Promise(resolve => setTimeout(resolve, CONFIG.GENERATION_DELAY)) + const newKey = generateSecureKey() + setGeneratedKey(newKey) + setIsGenerating(false) + }, []) + + const handleCopy = useCallback(async (text: string, _type: string) => { + try { + await navigator.clipboard.writeText(text) + setCopySuccess(true) + setTimeout(() => setCopySuccess(false), CONFIG.COPY_FEEDBACK_DURATION) + } catch (err) { + console.error('Failed to copy:', err) + } + }, []) + + const fullConfig = useMemo(() => { + return corsEnabled + ? `\ndefine('JWT_AUTH_SECRET_KEY', '${generatedKey}');\ndefine('JWT_AUTH_CORS_ENABLE', true);` + : `\ndefine('JWT_AUTH_SECRET_KEY', '${generatedKey}');` + }, [generatedKey, corsEnabled]) + + return ( + + + Setup Required + + } + > +
+ {/* Configuration Section */} +
+
+ + + {fullConfig} + +
+ +
+
+ + +
+ +
+ +
+
+ +
+ Important: Copy this configuration and add it to your wp-config.php + file. Keep it secure and never share it publicly. +
+
+
+
+ +
+
+
+ ) +} diff --git a/admin/ui/src/components/dashboard/system-environment.tsx b/admin/ui/src/components/dashboard/system-environment.tsx new file mode 100644 index 00000000..fa83e3a1 --- /dev/null +++ b/admin/ui/src/components/dashboard/system-environment.tsx @@ -0,0 +1,58 @@ +import { Loader2, X } from 'lucide-react' +import { InfoCard } from '@/components/ui/info-card' +import { StatusRow } from '@/components/ui/status-row' +import { Check } from '@/components/ui/check-icon' +import type { ConfigurationStatus } from '@/lib/wordpress-api' + +interface SystemEnvironmentProps { + configStatus: ConfigurationStatus | null +} + +export const SystemEnvironment = ({ configStatus }: SystemEnvironmentProps) => { + if (!configStatus) { + return ( + +
+ +
+
+ ) + } + + const { system } = configStatus + const allCompatible = system.php_compatible + + return ( + + + + {system.php_version} {system.pro_compatible ? '(Pro Compatible)' : '(Update Recommended)'} + + {system.php_compatible ? : } + + + {system.wordpress_version} (Supported) + + + + {system.php_memory_limit} (Sufficient) + + + + {system.mysql_version} + + + + {system.post_max_size} + + + + ) +} diff --git a/admin/ui/src/components/dashboard/token-dashboard.tsx b/admin/ui/src/components/dashboard/token-dashboard.tsx new file mode 100644 index 00000000..d09a7908 --- /dev/null +++ b/admin/ui/src/components/dashboard/token-dashboard.tsx @@ -0,0 +1,194 @@ +import { + Shield, + Eye, + RotateCcw, + Zap, + Globe, + TrendingUp, + ArrowRight, + CheckCircle, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { buildProUrl } from '@/lib/utils' + +interface TokenDashboardProps { + onBackToDashboard: () => void +} + +export const TokenDashboard = ({ onBackToDashboard: _onBackToDashboard }: TokenDashboardProps) => { + const proUrl = buildProUrl({ + source: 'token-dashboard', + medium: 'placeholder', + campaign: 'pro-upgrade', + content: 'unlock-features', + }) + + const features = [ + { + icon: Eye, + title: 'Real-time Token Visibility', + description: + 'Monitor all active JWT tokens with detailed dashboard insights and live status updates', + color: 'jwt-text-blue-600', + bgColor: 'jwt-bg-blue-50', + }, + { + icon: RotateCcw, + title: 'Refresh Token Mechanism', + description: + 'Automatic token rotation with secure refresh capabilities and seamless user experience', + color: 'jwt-text-emerald-600', + bgColor: 'jwt-bg-emerald-50', + }, + { + icon: Shield, + title: 'Instant Token Revocation', + description: + 'Auto-revoke on password/email/role changes for maximum security and threat prevention', + color: 'jwt-text-purple-600', + bgColor: 'jwt-bg-purple-50', + }, + { + icon: Zap, + title: 'Rate Limiting', + description: + 'Advanced rate limiting per IP address to prevent abuse and ensure system stability', + color: 'jwt-text-red-600', + bgColor: 'jwt-bg-red-50', + }, + { + icon: Globe, + title: 'Geo-IP Login Tracking', + description: + 'Identify and track login locations for enhanced security and suspicious activity detection', + color: 'jwt-text-orange-600', + bgColor: 'jwt-bg-orange-50', + }, + { + icon: TrendingUp, + title: 'Advanced Analytics', + description: + 'Detailed usage tracking with 50+ WordPress hooks and comprehensive reporting dashboard', + color: 'jwt-text-indigo-600', + bgColor: 'jwt-bg-indigo-50', + }, + ] + + const benefits = [ + 'Refresh tokens with automatic rotation', + 'Instant revocation on security events', + 'Advanced rate limiting protection', + 'Geo-IP tracking and analytics', + '50+ WordPress integration hooks', + 'Premium support and documentation', + ] + + return ( +
+ {/* Header Section */} +
+
+
+ +
+

+ Token Dashboard +

+

+ Real-time token management, refresh mechanisms, and advanced security features for + modern applications +

+
+
+ + {/* Features Grid */} +
+
+ {features.map((feature, index) => { + const Icon = feature.icon + const featureUrl = buildProUrl({ + source: 'token-dashboard', + medium: 'feature-card', + campaign: 'pro-upgrade', + content: feature.title.toLowerCase().replace(/\s+/g, '-'), + }) + + return ( + + +
+
+ +
+

+ {feature.title} +

+

{feature.description}

+
+
+ + + ) + })} +
+
+ + {/* CTA Section */} +
+ +
+
+
+ +
+

+ Unlock Professional Token Management +

+

+ Upgrade to JWT Authentication Pro for refresh tokens, instant revocation, rate + limiting, geo-IP tracking, and advanced analytics. Perfect for headless WordPress, + mobile apps, and SPAs. +

+ + {/* Benefits List */} +
+ {benefits.map((benefit, index) => ( +
+ + {benefit} +
+ ))} +
+ + + +

+ Starting at $59.99/year{' '} + â€ĸ 50+ WordPress hooks â€ĸ Premium support +

+
+ +
+
+ ) +} diff --git a/admin/ui/src/components/dashboard/topbar.tsx b/admin/ui/src/components/dashboard/topbar.tsx new file mode 100644 index 00000000..3e2ffd1c --- /dev/null +++ b/admin/ui/src/components/dashboard/topbar.tsx @@ -0,0 +1,68 @@ +import { Button } from '@/components/ui/button' +import { Rocket, BarChart3 } from 'lucide-react' +import { buildProUrl, getDynamicCTAText, getWeekNumber } from '@/lib/utils' + +interface TopbarProps { + currentPage: 'overview' | 'survey' | 'token-dashboard' + onPageChange: (page: 'overview' | 'survey' | 'token-dashboard') => void +} + +export const Topbar = ({ currentPage, onPageChange }: TopbarProps) => { + const weekNumber = getWeekNumber() + const ctaText = getDynamicCTAText('header') + const proUrl = buildProUrl({ + source: 'dashboard', + medium: 'header', + campaign: 'pro-upgrade', + content: `cta-week-${(weekNumber % 4) + 1}`, + }) + + return ( +
+
+
+
+
+ +

JWT Auth

+
+ + {/* Navigation */} + +
+ + +
+
+
+ ) +} diff --git a/admin/ui/src/components/survey/ConsentFlow.tsx b/admin/ui/src/components/survey/ConsentFlow.tsx new file mode 100644 index 00000000..dd9cccaa --- /dev/null +++ b/admin/ui/src/components/survey/ConsentFlow.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { Shield, Lock, ExternalLink } from 'lucide-react' + +interface ConsentFlowProps { + onAccept: () => void +} + +export const ConsentFlow = ({ onAccept }: ConsentFlowProps) => { + const [consentGiven, setConsentGiven] = useState(false) + + return ( + +
+
+
+ +
+

+ Help Us Improve JWT Authentication +

+

+ Your feedback helps us build features that matter. This quick survey takes 2 minutes and + you'll get 15% off any JWT Auth PRO subscription. +

+
+ +
+
+ +
+

Data Collection Notice

+

+ Your survey responses will be sent to our secure server to help improve the plugin. +

+

+ No personal information is required, and email sharing is completely optional. +

+
+
+
+ +
+
+ setConsentGiven(e.target.checked)} + className="jwt-h-4 jwt-w-4 jwt-text-blue-600 jwt-focus:ring-blue-500 jwt-border-gray-300 jwt-rounded jwt-flex-shrink-0" + /> + +
+ +

+ By participating, you agree to our{' '} + + Privacy Policy + +

+
+ +
+ +
+
+
+ ) +} diff --git a/admin/ui/src/components/survey/SuccessFlow.tsx b/admin/ui/src/components/survey/SuccessFlow.tsx new file mode 100644 index 00000000..b7d1a010 --- /dev/null +++ b/admin/ui/src/components/survey/SuccessFlow.tsx @@ -0,0 +1,103 @@ +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { CheckCircle, ExternalLink } from 'lucide-react' + +interface SuccessFlowProps { + discountCode: string + hasEmail: boolean + onReset: () => void +} + +export const SuccessFlow = ({ discountCode, hasEmail, onReset }: SuccessFlowProps) => { + const proUrl = `https://jwtauth.pro?utm_source=wp-admin&utm_medium=survey&utm_campaign=upgrade&utm_content=discount-${discountCode}` + + return ( + +
+
+
+ +
+

+ Thank You for Your Feedback! +

+

+ Your responses help us build better features.{' '} + {hasEmail + ? 'Check your email for your 15% discount code for JWT Auth PRO.' + : 'Consider sharing your email next time to receive 15% off JWT Auth PRO.'} +

+
+ + {/* Discount Code Section - Only show if user provided email */} + {hasEmail && ( +
+
+
+

+ Your 15% discount code for JWT Auth PRO has been sent +

+

+ Valid on any JWT Auth PRO subscription plan - check your inbox (and spam folder) +

+
+ +
+
+ )} + + {/* Pro Features Highlight */} +
+

+ What You Get with JWT Auth Pro: +

+
+
+
+ Token refresh capability +
+
+
+ Active token tracking +
+
+
+ + Multiple algorithms (RS256, ES256) + +
+
+
+ Usage analytics +
+
+
+ Priority support +
+
+
+ Advanced security features +
+
+
+ + {/* Action Buttons */} +
+ +
+
+
+ ) +} diff --git a/admin/ui/src/components/survey/SurveyForm.tsx b/admin/ui/src/components/survey/SurveyForm.tsx new file mode 100644 index 00000000..fddad37b --- /dev/null +++ b/admin/ui/src/components/survey/SurveyForm.tsx @@ -0,0 +1,464 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react' +import { wordpressAPI } from '@/lib/wordpress-api' +import type { SurveyData } from './SurveyPage' + +interface SurveyFormProps { + initialData: SurveyData + onSubmit: (data: SurveyData) => void + onBack: () => void +} + +export const SurveyForm = ({ initialData, onSubmit, onBack }: SurveyFormProps) => { + const [formData, setFormData] = useState(initialData) + const [errors, setErrors] = useState>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [currentStep, setCurrentStep] = useState(1) + + const totalSteps = 5 + + const validateCurrentStep = () => { + const newErrors: Record = {} + + switch (currentStep) { + case 1: + if (!formData.useCase) { + newErrors.useCase = 'Please select a use case' + } + if (formData.useCase === 'other' && !formData.useCaseOther) { + newErrors.useCaseOther = 'Please specify your use case' + } + break + case 2: + if (!formData.projectTimeline) { + newErrors.projectTimeline = 'Please select a project timeline' + } + break + case 3: + if (!formData.primaryChallenge) { + newErrors.primaryChallenge = 'Please select your primary challenge' + } + if (formData.primaryChallenge === 'other' && !formData.primaryChallengeOther) { + newErrors.primaryChallengeOther = 'Please specify your challenge' + } + break + case 4: + if (!formData.purchaseInterest) { + newErrors.purchaseInterest = 'Please select your interest level' + } + break + case 5: + // Email is optional, no validation needed + break + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleNext = () => { + if (validateCurrentStep()) { + if (currentStep < totalSteps) { + setCurrentStep(currentStep + 1) + setErrors({}) + } else { + handleSubmit() + } + } + } + + const handlePrevious = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1) + setErrors({}) + } else { + onBack() + } + } + + const handleSubmit = async () => { + setIsSubmitting(true) + + try { + const result = await wordpressAPI.submitSurvey(formData) + + if (result.success) { + await wordpressAPI.markSurveyCompleted() + onSubmit(formData) + } else { + setErrors({ submit: result.message || 'Failed to submit survey' }) + } + } catch (error) { + console.error('Survey submission error:', error) + onSubmit(formData) + } finally { + setIsSubmitting(false) + } + } + + const handleInputChange = (field: keyof SurveyData, value: string | boolean) => { + setFormData(prev => ({ ...prev, [field]: value })) + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: '' })) + } + } + + const useCaseOptions = [ + { value: 'mobile-app', label: 'Mobile app backend (iOS/Android)' }, + { value: 'headless-wp', label: 'Headless WordPress (React/Vue/Next.js)' }, + { value: 'woocommerce', label: 'WooCommerce API integration' }, + { value: 'custom-dashboard', label: 'Custom dashboard/portal' }, + { value: 'testing', label: 'Still testing/exploring' }, + { value: 'other', label: 'Other' }, + ] + + const timelineOptions = [ + { value: 'live', label: "It's already live" }, + { value: 'this-month', label: 'Launching this month' }, + { value: 'next-months', label: 'Next 2-3 months' }, + { value: 'experimenting', label: 'Just experimenting' }, + ] + + const challengeOptions = [ + { value: 'token-tracking', label: "Can't track active tokens/users" }, + { value: 'token-refresh', label: 'Need token refresh capability' }, + { value: 'token-revocation', label: 'Need token revocation system' }, + { value: 'token-analytics', label: 'Need token traceability and history' }, + { value: 'no-issues', label: 'No issues - it works fine' }, + { value: 'other', label: 'Other' }, + ] + + const purchaseOptions = [ + { value: 'very-interested', label: 'Very interested - tell me more' }, + { value: 'interested-specific', label: 'Interested if it solves my specific need' }, + { value: 'maybe-later', label: 'Maybe later when project is further along' }, + { value: 'happy-free', label: 'Happy with free version' }, + ] + + const renderStep = () => { + switch (currentStep) { + case 1: + return renderUseCase() + case 2: + return renderTimeline() + case 3: + return renderChallenge() + case 4: + return renderPurchaseInterest() + case 5: + return renderEmailCollection() + default: + return null + } + } + + const renderUseCase = () => ( + +
+
+

+ What are you building with JWT Authentication? +

+

Help us understand your primary use case

+
+
+ {useCaseOptions.map(option => ( + + ))} + {formData.useCase === 'other' && ( +
+