Newer
Older
cortex-hub / frontend / src / features / auth / pages / LoginPage.js
import React, { useState, useEffect } from 'react';
import { login, loginLocal, getUserStatus, logout, getAuthConfig } from '../../../services/apiService';

const LoginPage = () => {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [oidcEnabled, setOidcEnabled] = useState(false);
  
  // Local login state
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [successMessage, setSuccessMessage] = useState('');

  useEffect(() => {
    // 1. Check if OIDC is enabled
    const checkConfig = async () => {
      try {
        const config = await getAuthConfig();
        setOidcEnabled(config.oidc_configured);
      } catch (err) {
        console.error("Failed to fetch auth config", err);
      }
    };
    checkConfig();

    // 2. Handle OIDC callback or persistent session
    const params = new URLSearchParams(window.location.search);
    const userIdFromUrl = params.get('user_id');
    const storedUserId = localStorage.getItem('userId');
    const userId = userIdFromUrl || storedUserId;
    const isLinked = params.get("linked") === "true";

    if (userId) {
      setIsLoading(true);
      const fetchUserDetails = async () => {
        try {
          const userStatus = await getUserStatus(userId);
          setUser(userStatus);
          localStorage.setItem('userId', userStatus.id);
          window.history.replaceState({}, document.title, window.location.pathname);
          if (isLinked) {
            setSuccessMessage("Social identity successfully linked to your existing account!");
          }
           setTimeout(() => {
             window.location.href = "/swarm-control";
           }, 1500);
        } catch (err) {
          setError('Failed to get user status. Please try again.');
          localStorage.removeItem('userId'); // Clear invalid ID
          console.error(err);
        } finally {
          setIsLoading(false);
        }
      };
      fetchUserDetails();
    }
  }, []);

  const handleOidcLogin = () => {
    login();
  };

  const handleLocalLogin = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    setError(null);
    try {
      const result = await loginLocal(email, password);
      setUser({ id: result.user_id, email: result.email });
      localStorage.setItem('userId', result.user_id);
      // Redirect to home page after successful local login
      setTimeout(() => {
        window.location.href = "/";
      }, 1000);
    } catch (err) {
      setError(err.message || 'Login failed. Please check your credentials.');
    } finally {
      setIsLoading(false);
    }
  };

  const handleLogout = async () => {
    setIsLoading(true);
    try {
      await logout();
      localStorage.removeItem('userId');
      setUser(null);
      setError(null);
    } catch (err) {
      setError('Failed to log out. Please try again.');
      console.error(err);
    } finally {
      setIsLoading(false);
    }
  };

  const renderContent = () => {
    if (isLoading) {
      return (
        <div className="flex flex-col items-center justify-center p-4">
          <svg className="animate-spin h-10 w-10 mb-4 text-blue-500" viewBox="0 0 24 24">
            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
          </svg>
          <span className="text-gray-600 dark:text-gray-400 font-medium">Authenticating...</span>
        </div>
      );
    }

    if (user) {
      return (
        <div className="p-4 text-center">
          <div className="mb-4 inline-flex items-center justify-center w-16 h-16 bg-green-100 dark:bg-green-900 rounded-full">
            <svg className="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
            </svg>
          </div>
          <h3 className="text-2xl font-bold mb-2 text-gray-900 dark:text-white">Welcome Back!</h3>
          <p className="text-gray-600 dark:text-gray-400 mb-6">{user.email}</p>
          <button
            onClick={handleLogout}
            className="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition-all shadow-md focus:outline-none focus:ring-2 focus:ring-red-500"
          >
            Sign Out
          </button>
        </div>
      );
    }

    return (
      <div className="w-full">
        <div className="mb-8">
          <h2 className="text-3xl font-extrabold text-gray-900 dark:text-white mb-2">Sign In</h2>
          <p className="text-gray-500 dark:text-gray-400">Access your Cortex Hub dashboard</p>
        </div>

        {error && (
          <div className="mb-6 p-4 bg-red-50 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-400 text-sm text-left">
            {error}
          </div>
        )}

        {successMessage && (
          <div className="mb-6 p-4 bg-green-50 dark:bg-green-900/30 border-l-4 border-green-500 text-green-700 dark:text-green-400 text-sm text-left">
            {successMessage}
          </div>
        )}

        <form onSubmit={handleLocalLogin} className="space-y-4 text-left">
          <div>
            <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email Address</label>
            <input
              type="email"
              id="email"
              name="email"
              required
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="w-full px-4 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:text-white transition-all"
              placeholder="admin@example.com"
              autoComplete="email"
            />
          </div>
          <div>
            <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
            <input
              type="password"
              id="password"
              name="password"
              required
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              className="w-full px-4 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:text-white transition-all"
              placeholder="••••••••"
              autoComplete="current-password"
            />
          </div>
          <button
            type="submit"
            className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg shadow-lg hover:shadow-xl transition-all transform hover:-translate-y-0.5 active:translate-y-0"
          >
            Sign In with Password
          </button>
        </form>

        {oidcEnabled && (
          <div className="mt-8">
            <div className="relative flex items-center justify-center mb-6">
              <div className="flex-grow border-t border-gray-300 dark:border-gray-600"></div>
              <span className="flex-shrink mx-4 text-gray-400 text-sm uppercase font-bold tracking-wider">Or</span>
              <div className="flex-grow border-t border-gray-300 dark:border-gray-600"></div>
            </div>
            <button
              onClick={handleOidcLogin}
              className="w-full flex items-center justify-center bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-white font-semibold py-3 px-4 rounded-lg shadow-sm transition-all"
            >
              <svg className="w-5 h-5 mr-3" viewBox="0 0 48 48">
                <path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"></path>
                <path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"></path>
                <path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"></path>
                <path fill="#1976D2" d="M43.611,20.083L43.611,20.083L42,20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"></path>
              </svg>
              Sign in with SSO
            </button>
          </div>
        )}
      </div>
    );
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-black p-4">
      <div className="bg-white dark:bg-gray-800 p-10 rounded-2xl shadow-2xl max-w-md w-full border border-gray-100 dark:border-gray-700">
        <div className="mb-10 flex justify-center">
           <div className="w-16 h-16 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg transform -rotate-6">
              <span className="text-white text-3xl font-black">C</span>
           </div>
        </div>
        {renderContent()}
      </div>
    </div>
  );
};

export default LoginPage;