diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx
index 59e3257..e3caf95 100644
--- a/admin-spa/src/App.tsx
+++ b/admin-spa/src/App.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { HashRouter, Routes, Route, NavLink, useLocation, useParams } from 'react-router-dom';
+import { HashRouter, Routes, Route, NavLink, useLocation, useParams, Navigate } from 'react-router-dom';
import Dashboard from '@/routes/Dashboard';
import DashboardRevenue from '@/routes/Dashboard/Revenue';
import DashboardOrders from '@/routes/Dashboard/Orders';
@@ -19,6 +19,7 @@ import ProductAttributes from '@/routes/Products/Attributes';
import CouponsIndex from '@/routes/Coupons';
import CouponNew from '@/routes/Coupons/New';
import CustomersIndex from '@/routes/Customers';
+import { Login } from '@/routes/Login';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { LayoutDashboard, ReceiptText, Package, Tag, Users, Settings as SettingsIcon, Maximize2, Minimize2, Loader2 } from 'lucide-react';
import { Toaster } from 'sonner';
@@ -394,13 +395,63 @@ function Shell() {
);
}
+function AuthWrapper() {
+ const [isAuthenticated, setIsAuthenticated] = useState(
+ window.WNW_CONFIG?.isAuthenticated ?? true
+ );
+ const [isChecking, setIsChecking] = useState(window.WNW_CONFIG?.standaloneMode ?? false);
+ const location = useLocation();
+
+ useEffect(() => {
+ if (window.WNW_CONFIG?.standaloneMode) {
+ fetch(window.WNW_CONFIG.restUrl + '/auth/check', {
+ credentials: 'include',
+ })
+ .then(res => res.json())
+ .then(data => {
+ setIsAuthenticated(data.authenticated);
+ if (data.authenticated && data.user) {
+ window.WNW_CONFIG.currentUser = data.user;
+ }
+ })
+ .catch(() => setIsAuthenticated(false))
+ .finally(() => setIsChecking(false));
+ }
+ }, []);
+
+ if (isChecking) {
+ return (
+
+
+
+ );
+ }
+
+ if (window.WNW_CONFIG?.standaloneMode && !isAuthenticated && location.pathname !== '/login') {
+ return ;
+ }
+
+ if (location.pathname === '/login' && isAuthenticated) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+}
+
export default function App() {
return (
-
-
-
+
+ {window.WNW_CONFIG?.standaloneMode && (
+ } />
+ )}
+ } />
+
svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/admin-spa/src/routes/Login.tsx b/admin-spa/src/routes/Login.tsx
new file mode 100644
index 0000000..101afeb
--- /dev/null
+++ b/admin-spa/src/routes/Login.tsx
@@ -0,0 +1,153 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Loader2, ArrowLeft } from 'lucide-react';
+import { __ } from '@/lib/i18n';
+
+export function Login() {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+ const navigate = useNavigate();
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ setError('');
+
+ try {
+ const response = await fetch(window.WNW_CONFIG.restUrl + '/auth/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify({ username, password }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ // Update global config
+ window.WNW_CONFIG.isAuthenticated = true;
+ window.WNW_CONFIG.currentUser = data.user;
+ window.WNW_CONFIG.nonce = data.nonce;
+
+ // Redirect to dashboard
+ navigate('/dashboard');
+
+ // Reload to ensure all auth state is fresh
+ window.location.reload();
+ } else {
+ setError(data.message || __('Invalid username or password'));
+ }
+ } catch (err) {
+ console.error('Login error:', err);
+ setError(__('Login failed. Please try again.'));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ {/* Logo */}
+
+
+ WooNooW
+
+
+ {__('Sign in to your admin dashboard')}
+
+
+
+ {/* Error Alert */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Login Form */}
+
+
+ {/* Footer Links */}
+
+
+
+ {/* Site Info */}
+
+ {window.WNW_CONFIG.siteName}
+
+
+
+ );
+}
diff --git a/admin/index.php b/admin/index.php
new file mode 100644
index 0000000..8069856
--- /dev/null
+++ b/admin/index.php
@@ -0,0 +1,84 @@
+ $user->ID,
+ 'name' => $user->display_name,
+ 'email' => $user->user_email,
+ 'avatar' => get_avatar_url( $user->ID ),
+ ];
+}
+
+// Get asset URLs
+$plugin_url = plugins_url( '', dirname( __FILE__ ) );
+$asset_url = $plugin_url . '/admin-spa/dist';
+$css_url = $asset_url . '/app.css';
+$js_url = $asset_url . '/app.js';
+
+// Add cache busting
+$version = defined( 'WP_DEBUG' ) && WP_DEBUG ? time() : '1.0.0';
+$css_url .= '?ver=' . $version;
+$js_url .= '?ver=' . $version;
+?>
+
+
+
+
+
+
+ WooNooW Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/includes/Api/AuthController.php b/includes/Api/AuthController.php
new file mode 100644
index 0000000..69210c9
--- /dev/null
+++ b/includes/Api/AuthController.php
@@ -0,0 +1,114 @@
+get_param( 'username' ) );
+ $password = $request->get_param( 'password' );
+
+ if ( empty( $username ) || empty( $password ) ) {
+ return new WP_REST_Response( [
+ 'success' => false,
+ 'message' => __( 'Username and password are required', 'woonoow' ),
+ ], 400 );
+ }
+
+ // Authenticate user
+ $user = wp_authenticate( $username, $password );
+
+ if ( is_wp_error( $user ) ) {
+ return new WP_REST_Response( [
+ 'success' => false,
+ 'message' => __( 'Invalid username or password', 'woonoow' ),
+ ], 401 );
+ }
+
+ // Check if user has WooCommerce permissions
+ if ( ! user_can( $user, 'manage_woocommerce' ) ) {
+ return new WP_REST_Response( [
+ 'success' => false,
+ 'message' => __( 'You do not have permission to access this area', 'woonoow' ),
+ ], 403 );
+ }
+
+ // Set auth cookie
+ wp_set_auth_cookie( $user->ID, true );
+
+ // Return user data and new nonce
+ return new WP_REST_Response( [
+ 'success' => true,
+ 'user' => [
+ 'id' => $user->ID,
+ 'name' => $user->display_name,
+ 'email' => $user->user_email,
+ 'avatar' => get_avatar_url( $user->ID ),
+ ],
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ ], 200 );
+ }
+
+ /**
+ * Logout endpoint
+ *
+ * @return WP_REST_Response Response object
+ */
+ public static function logout(): WP_REST_Response {
+ wp_logout();
+
+ return new WP_REST_Response( [
+ 'success' => true,
+ 'message' => __( 'Logged out successfully', 'woonoow' ),
+ ], 200 );
+ }
+
+ /**
+ * Check auth status
+ *
+ * @return WP_REST_Response Response object
+ */
+ public static function check(): WP_REST_Response {
+ if ( ! is_user_logged_in() ) {
+ return new WP_REST_Response( [
+ 'authenticated' => false,
+ ], 200 );
+ }
+
+ $user = wp_get_current_user();
+
+ // Check WooCommerce permission
+ if ( ! current_user_can( 'manage_woocommerce' ) ) {
+ return new WP_REST_Response( [
+ 'authenticated' => false,
+ 'message' => __( 'Insufficient permissions', 'woonoow' ),
+ ], 200 );
+ }
+
+ return new WP_REST_Response( [
+ 'authenticated' => true,
+ 'user' => [
+ 'id' => $user->ID,
+ 'name' => $user->display_name,
+ 'email' => $user->user_email,
+ 'avatar' => get_avatar_url( $user->ID ),
+ ],
+ ], 200 );
+ }
+}
diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php
index 5e98b52..023cf05 100644
--- a/includes/Api/Routes.php
+++ b/includes/Api/Routes.php
@@ -6,6 +6,7 @@ use WP_REST_Response;
use WooNooW\Api\CheckoutController;
use WooNooW\Api\OrdersController;
use WooNooW\Api\AnalyticsController;
+use WooNooW\Api\AuthController;
class Routes {
public static function init() {
@@ -14,6 +15,26 @@ class Routes {
add_action('rest_api_init', function () {
$namespace = 'woonoow/v1';
+
+ // Auth endpoints (public - no permission check)
+ register_rest_route( $namespace, '/auth/login', [
+ 'methods' => 'POST',
+ 'callback' => [ AuthController::class, 'login' ],
+ 'permission_callback' => '__return_true',
+ ] );
+
+ register_rest_route( $namespace, '/auth/logout', [
+ 'methods' => 'POST',
+ 'callback' => [ AuthController::class, 'logout' ],
+ 'permission_callback' => '__return_true',
+ ] );
+
+ register_rest_route( $namespace, '/auth/check', [
+ 'methods' => 'GET',
+ 'callback' => [ AuthController::class, 'check' ],
+ 'permission_callback' => '__return_true',
+ ] );
+
// Defer to controllers to register their endpoints
CheckoutController::register();
OrdersController::register();