Skip to content

Authentication Pages

Purpose: Exhaustive page-level documentation for all 40+ routes
Repository: machineagents-fe/src/app/
Last Updated: 2025-12-29
Coverage: In Progress (Phase 2 of Exhaustive Documentation)


1. Login Page

File: src/app/components/Login.tsx (441 lines)
Route: /login
Type: Public
Purpose: User authentication with reCAPTCHA protection


State Variables (6)

  1. email (string) - User email/username input
  2. password (string) - User password input
  3. showPassword (boolean) - Toggle password visibility
  4. error (string) - Error message display
  5. recaptchaToken (string) - Google reCAPTCHA v2 token
  6. recaptchaRef (useRef<any>) - reCAPTCHA instance reference
  7. containerRef (useRef<HTMLDivElement>) - reCAPTCHA container DOM ref

Form Validation

const isFormValid =
  email.trim() !== "" && password.trim() !== "" && recaptchaToken !== "";

Requirements: -✅ Email/username not empty

  • ✅ Password not empty
  • ✅ reCAPTCHA completed

reCAPTCHA Integration

Configuration:

const RECAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY as string;

Rendering Logic:

useEffect(() => {
  const renderRecaptcha = () => {
    if (!window.grecaptcha || !containerRef.current) return;

    // Reset existing instance
    if (recaptchaRef.current !== null && window.grecaptcha.reset) {
      try {
        window.grecaptcha.reset(recaptchaRef.current);
      } catch (error) {
        // Ignore reset errors
      }
    }

    // Clear container completely
    if (containerRef.current) {
      containerRef.current.innerHTML = "";
      const newDiv = document.createElement("div");
      containerRef.current.appendChild(newDiv);
    }

    recaptchaRef.current = null;

    try {
      const newDiv = containerRef.current?.firstChild as HTMLElement;
      if (newDiv) {
        recaptchaRef.current = window.grecaptcha.render(newDiv, {
          sitekey: RECAPTCHA_SITE_KEY,
          callback: (token: string) => setRecaptchaToken(token),
          "expired-callback": () => setRecaptchaToken(""),
          theme: "dark",
        });
      }
    } catch (error) {
      console.error("reCAPTCHA render error:", error);
      // Retry after 1 second on error
      setTimeout(() => {
        if (containerRef.current) {
          containerRef.current.innerHTML = "";
          renderRecaptcha();
        }
      }, 1000);
    }
  };

  const initRecaptcha = () => {
    if (window.grecaptcha && window.grecaptcha.render) {
      window.grecaptcha.ready(() => {
        renderRecaptcha();
      });
    } else {
      // Wait 500ms and retry if not ready
      setTimeout(initRecaptcha, 500);
    }
  };

  // Start initialization after 100ms
  const timeoutId = setTimeout(initRecaptcha, 100);

  return () => {
    clearTimeout(timeoutId);
    if (
      recaptchaRef.current !== null &&
      window.grecaptcha &&
      window.grecaptcha.reset
    ) {
      try {
        window.grecaptcha.reset(recaptchaRef.current);
      } catch (error) {
        // Ignore cleanup errors
      }
    }
    if (containerRef.current) {
      containerRef.current.innerHTML = "";
    }
    recaptchaRef.current = null;
    setRecaptchaToken("");
  };
}, []);

Features:

  • ✅ Automatic retry on render failure
  • ✅ Dark theme
  • ✅ Proper cleanup on unmount
  • ✅ Token expiration handling
  • ✅ Container reset to avoid duplicate renders

Admin Redirect Feature

Purpose: Allow superadmin to log in as user via URL parameters

Implementation:

useEffect(() => {
  const urlParams = new URLSearchParams(window.location.search);
  const isAdminRedirect = urlParams.get("adminRedirect");

  if (isAdminRedirect === "true") {
    const token = urlParams.get("token");
    const userId = urlParams.get("user_id");
    const sessionId = urlParams.get("session_id");
    const email = urlParams.get("email");

    if (token && userId) {
      // Set session storage with the provided data
      sessionStorage.setItem("isLoggedIn", "true");
      sessionStorage.setItem("authToken", token);
      sessionStorage.setItem("user_id", userId);
      sessionStorage.setItem("session_id", sessionId || generateSessionId());
      sessionStorage.setItem("userEmail", email || "");
      sessionStorage.setItem("connectedFromSuperAdmin", "true");

      // Clean URL and redirect to chatbots
      window.history.replaceState({}, document.title, window.location.pathname);
      router.push("/chatbots");
      return;
    }
  }
}, [router]);

URL Format:

/login?adminRedirect=true&token=xxx&user_id=yyy&session_id=zzz&email=user@example.com

Login Flow

API Endpoint: POST /v2/login

Request:

const formData = new FormData();
formData.append("email", email);
formData.append("password", password);
formData.append("recaptcha_token", recaptchaToken);

const response = await fetch(
  `${process.env.NEXT_PUBLIC_BACKEND_URL}/v2/login`,
  {
    method: "POST",
    body: formData,
  }
);

Success Response:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user_id": "user_12345",
  "session_id": "session_abc123"
}

Session Storage on Success:

sessionStorage.setItem("isLoggedIn", "true");
sessionStorage.setItem("authToken", data.token);
sessionStorage.setItem("user_id", data.user_id);
sessionStorage.setItem("session_id", data.session_id || generateSessionId());
sessionStorage.setItem("userEmail", email);

Payment Plan Redirect

Purpose: Resume payment flow after login if user was redirected from pricing page

Logic:

// Check if there's a stored plan for payment
const storedPlan = sessionStorage.getItem("selectedPlanForPayment");
if (storedPlan) {
  try {
    const plan = JSON.parse(storedPlan);
    sessionStorage.removeItem("selectedPlanForPayment"); // Clean up

    const planData = {
      ...plan,
      category: plan.name.toLowerCase(),
    };

    // Free plan - skip payment
    if (plan.name == "Free") {
      router.push("/chatbots");
      return;
    }

    // Paid plan - redirect to payment
    const queryParams = new URLSearchParams({
      plan: encodeURIComponent(JSON.stringify(planData)),
    });
    router.push(`/payment?${queryParams.toString()}`);
  } catch (error) {
    console.error("Error parsing stored plan:", error);
    sessionStorage.removeItem("selectedPlanForPayment");
    router.push("/chatbots");
  }
} else {
  router.push("/chatbots");
}

Error Handling

Error Response Parsing:

const errorData = await response.json();

let errorMessage = "Invalid email or password";

if (errorData.detail) {
  if (
    typeof errorData.detail === "string" &&
    errorData.detail.startsWith("{")
  ) {
    try {
      // Backend sometimes returns JSON string in detail field
      const parsed = JSON.parse(errorData.detail);
      errorMessage = parsed.detail || errorData.detail;
    } catch {
      errorMessage = errorData.detail;
    }
  } else {
    errorMessage = errorData.detail;
  }
}

toast.error(errorMessage);
resetRecaptcha(); // Reset reCAPTCHA on error

reCAPTCHA Reset:

const resetRecaptcha = () => {
  if (
    window.grecaptcha &&
    window.grecaptcha.reset &&
    recaptchaRef.current !== null
  ) {
    try {
      window.grecaptcha.reset(recaptchaRef.current);
    } catch (error) {
      console.error("Error resetting reCAPTCHA:", error);
    }
  }
  setRecaptchaToken("");
};

Auto-Redirect for LoggedIn Users

Purpose: Redirect to dashboard if already authenticated

useEffect(() => {
  const isLoggedIn = sessionStorage.getItem("isLoggedIn");
  const authToken = sessionStorage.getItem("authToken");

  // Check both isLoggedIn flag and authToken to ensure user is actually logged in
  if (isLoggedIn === "true" && authToken) {
    router.push("/chatbots");
  }
}, [router]);

UI Features

  1. Password Toggle:
<button onClick={() => setShowPassword(!showPassword)}>
  {showPassword ? <EyeSlashIcon /> : <EyeIcon />}
</button>
  1. Disabled Submit Button:
<button
  type="submit"
  disabled={!isFormValid}
  className={
    isFormValid
      ? "bg-[#FF6622] hover:bg-orange-600 cursor-pointer"
      : "bg-gray-600 cursor-not-allowed opacity-50"
  }
>
  Log in
</button>
  1. Toast Notifications:
<ToastContainer position="top-right" autoClose={3000} theme="dark" />
  1. Navigation Links:
  2. Forgot Password: /forgot-password
  3. Sign Up: /signup

Environment Variables

NEXT_PUBLIC_BACKEND_URL=https://api.machineavatars.com
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=6LeXXXXXXXXXXXXXXXXXXXXXXXXX

Dependencies

  • next/link - Client-side navigation
  • next/script - reCAPTCHA script loading
  • next/navigation (useRouter) - Programmatic navigation
  • react (useState, useEffect, useRef)
  • react-toastify - Error/success notifications
  • Google reCAPTCHA v2

Security Features

  1. reCAPTCHA v2 - Bot protection
  2. Password masking - Hidden by default
  3. HTTPS required - Backend URL must be HTTPS
  4. Token expiration - reCAPTCHA tokens expire after 2 minutes
  5. Error message sanitization - Prevents XSS in error display

Session ID Generation

const generateSessionId = () => {
  return "session_" + Math.random().toString(36).substr(2, 9);
};

Format: session_xxxxxxxxx (9 random characters)


    • useSessionData
    • Auth endpoints

Status: 5/40 pages documented
Next: Chatbot creation and interface pages