monorepo/web/maplepress-frontend/docs/API/ADMIN_API.md

22 KiB

Admin API Implementation (Account Management)

This document describes the implementation of the Admin API endpoints for the MaplePress frontend, integrated with the MaplePress backend API.

Overview

The Admin API endpoints provide administrative operations for managing user accounts, specifically handling account lockouts that occur due to failed login attempts. These endpoints implement CWE-307 protection (Improper Restriction of Excessive Authentication Attempts) by allowing administrators to check lock status and manually unlock accounts.

⚠️ SECURITY: These endpoints require admin authentication and should only be accessible to users with admin or root roles.

Backend API Endpoints

Check Account Lock Status

Endpoint: GET /api/v1/admin/account-status Authentication: Required (JWT token with admin role) Query Parameters: email (required)

Unlock Locked Account

Endpoint: POST /api/v1/admin/unlock-account Authentication: Required (JWT token with admin role)

Source Files:

  • cloud/maplepress-backend/internal/interface/http/handler/admin/account_status_handler.go
  • cloud/maplepress-backend/internal/interface/http/handler/admin/unlock_account_handler.go

Request/Response Structures

Check Account Status Request

GET /api/v1/admin/account-status?email=user@example.com

Headers Required:

  • Authorization: JWT {access_token} (admin role required)

Check Account Status Response

{
  "email": "user@example.com",
  "is_locked": true,
  "failed_attempts": 5,
  "remaining_time": "5 minutes 30 seconds",
  "remaining_seconds": 330
}

When Account Not Locked:

{
  "email": "user@example.com",
  "is_locked": false,
  "failed_attempts": 2
}

Unlock Account Request

{
  "email": "user@example.com"
}

Headers Required:

  • Content-Type: application/json
  • Authorization: JWT {access_token} (admin role required)

Unlock Account Response

{
  "success": true,
  "message": "Account unlocked successfully",
  "email": "user@example.com"
}

When Account Not Locked:

{
  "success": true,
  "message": "Account is not locked",
  "email": "user@example.com"
}

Frontend Implementation

AdminService (src/services/API/AdminService.js)

Handles all admin operations for account management.

Key Features:

  • Check account lock status with detailed information
  • Unlock locked accounts (with security event logging)
  • Helper to check if account needs unlocking
  • Client-side email validation
  • Remaining time formatting
  • Admin role enforcement with clear error messages

Methods:

getAccountStatus(email)

Check if a user account is locked due to failed login attempts.

import AdminService from './services/API/AdminService';

const status = await AdminService.getAccountStatus("user@example.com");
console.log(status);
// Output:
// {
//   email: "user@example.com",
//   isLocked: true,
//   failedAttempts: 5,
//   remainingTime: "5 minutes 30 seconds",
//   remainingSeconds: 330
// }

Parameters:

  • email (string, required): User's email address to check

Returns:

{
  email: string,              // User's email
  isLocked: boolean,          // Whether account is locked
  failedAttempts: number,     // Number of failed login attempts
  remainingTime: string,      // Human-readable time until unlock
  remainingSeconds: number    // Seconds until automatic unlock
}

Throws:

  • "Email is required" - If email is missing
  • "Email cannot be empty" - If email is empty after trimming
  • "Invalid email format" - If email format is invalid
  • "Admin authentication required. Please log in with admin credentials." - Missing/invalid admin token (401)
  • "Access denied. Admin privileges required for this operation." - User is not admin (403)

unlockAccount(email)

Unlock a user account that has been locked due to failed login attempts.

const result = await AdminService.unlockAccount("user@example.com");
console.log(result);
// Output:
// {
//   success: true,
//   message: "Account unlocked successfully",
//   email: "user@example.com"
// }

⚠️ SECURITY EVENT: This operation logs a security event (ACCOUNT_UNLOCKED) with the admin user ID who performed the unlock operation. This creates an audit trail for security compliance.

Parameters:

  • email (string, required): User's email address to unlock

Returns:

{
  success: boolean,
  message: string,
  email: string
}

Throws:

  • "Email is required" - If email is missing
  • "Email cannot be empty" - If email is empty after trimming
  • "Invalid email format" - If email format is invalid
  • "Account is not currently locked." - If account is not locked
  • "Admin authentication required. Please log in with admin credentials." - Missing/invalid admin token (401)
  • "Access denied. Admin privileges required for this operation." - User is not admin (403)

needsUnlock(email)

Check if an account needs unlocking (is locked with remaining time).

const needs = await AdminService.needsUnlock("user@example.com");
console.log(needs); // true or false

Parameters:

  • email (string, required): User's email address to check

Returns: boolean - True if account is locked and needs admin unlock

Use Case: Check before showing "Unlock Account" button in admin UI.

validateEmail(email)

Validate email format.

const result = AdminService.validateEmail("user@example.com");
console.log(result); // { valid: true, error: null }

const invalid = AdminService.validateEmail("invalid-email");
console.log(invalid); // { valid: false, error: "Invalid email format" }

Returns: { valid: boolean, error: string|null }

Validation Rules:

  • Required (non-empty)
  • Valid email format (user@domain.com)
  • Maximum 255 characters

formatRemainingTime(seconds)

Format remaining time for display.

const formatted = AdminService.formatRemainingTime(330);
console.log(formatted); // "5 minutes 30 seconds"

const oneHour = AdminService.formatRemainingTime(3661);
console.log(oneHour); // "1 hour 1 minute 1 second"

Returns: string (e.g., "5 minutes 30 seconds", "1 hour", "30 seconds")

Data Flow

Check Account Status Flow

Admin provides email to check
    ↓
AdminService.getAccountStatus()
    ↓
Validate email format (client-side)
    ↓
ApiClient.get() with JWT token (admin role)
    ↓
Token automatically refreshed if needed
    ↓
GET /api/v1/admin/account-status?email=...
    ↓
Backend checks Redis for lock status
    ↓
Backend returns lock info and failed attempts
    ↓
AdminService transforms response
    ↓
Component displays lock status

Unlock Account Flow

Admin requests account unlock
    ↓
AdminService.unlockAccount()
    ↓
Validate email format (client-side)
    ↓
ApiClient.post() with JWT token (admin role)
    ↓
Token automatically refreshed if needed
    ↓
POST /api/v1/admin/unlock-account
    ↓
Backend checks if account is locked
    ↓
Backend clears lock status in Redis
    ↓
Backend logs security event (ACCOUNT_UNLOCKED)
    ↓
Backend returns success response
    ↓
AdminService transforms response
    ↓
Component displays unlock confirmation

Error Handling

Error Types

Error Condition Response Frontend Behavior
Missing admin authentication 401 Unauthorized "Admin authentication required."
Insufficient privileges 403 Forbidden "Access denied. Admin privileges required."
Invalid email 400 Bad Request Specific validation error
Account not locked (unlock) 200 OK "Account is not locked" (success)
Server error 500 Internal Server Error Generic error message

Usage Examples

Admin Panel - Check Account Status

import React, { useState } from 'react';
import AdminService from '../../services/API/AdminService';

function AccountStatusChecker() {
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState(null);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleCheck = async (e) => {
    e.preventDefault();
    setError('');
    setStatus(null);
    setLoading(true);

    // Validate before sending
    const validation = AdminService.validateEmail(email);
    if (!validation.valid) {
      setError(validation.error);
      setLoading(false);
      return;
    }

    try {
      const accountStatus = await AdminService.getAccountStatus(email);
      setStatus(accountStatus);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="account-status-checker">
      <h2>Check Account Lock Status</h2>

      <form onSubmit={handleCheck}>
        <div>
          <label>Email Address:</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="user@example.com"
            maxLength={255}
          />
        </div>

        {error && <p className="error">{error}</p>}

        <button type="submit" disabled={loading}>
          {loading ? 'Checking...' : 'Check Status'}
        </button>
      </form>

      {status && (
        <div className="status-result">
          <h3>Account Status for {status.email}</h3>

          {status.isLocked ? (
            <div className="locked-status">
              <p className="warning">🔒 Account is LOCKED</p>
              <p>Failed Attempts: {status.failedAttempts}</p>
              <p>Automatic Unlock In: {status.remainingTime}</p>
              <p>({status.remainingSeconds} seconds remaining)</p>
            </div>
          ) : (
            <div className="unlocked-status">
              <p className="success"> Account is NOT locked</p>
              <p>Failed Attempts: {status.failedAttempts}</p>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default AccountStatusChecker;

Admin Panel - Unlock Account

import React, { useState } from 'react';
import AdminService from '../../services/API/AdminService';

function AccountUnlocker() {
  const [email, setEmail] = useState('');
  const [result, setResult] = useState(null);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleUnlock = async (e) => {
    e.preventDefault();
    setError('');
    setResult(null);
    setLoading(true);

    // Validate before sending
    const validation = AdminService.validateEmail(email);
    if (!validation.valid) {
      setError(validation.error);
      setLoading(false);
      return;
    }

    // Confirm action
    const confirmed = confirm(
      `Are you sure you want to unlock the account for "${email}"?\n\n` +
      `This will:\n` +
      `- Clear all failed login attempts\n` +
      `- Remove the account lock immediately\n` +
      `- Log a security event with your admin ID\n\n` +
      `Continue?`
    );

    if (!confirmed) {
      setLoading(false);
      return;
    }

    try {
      const unlockResult = await AdminService.unlockAccount(email);
      setResult(unlockResult);

      // Clear form on success
      if (unlockResult.success) {
        setEmail('');
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="account-unlocker">
      <h2>Unlock User Account</h2>
      <p className="warning">
        ⚠️ Admin action: This operation will be logged for security audit.
      </p>

      <form onSubmit={handleUnlock}>
        <div>
          <label>Email Address:</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="user@example.com"
            maxLength={255}
          />
        </div>

        {error && <p className="error">{error}</p>}

        {result && (
          <div className={result.success ? "success" : "error"}>
            <p>{result.message}</p>
            <p>Email: {result.email}</p>
          </div>
        )}

        <button type="submit" disabled={loading}>
          {loading ? 'Unlocking...' : 'Unlock Account'}
        </button>
      </form>
    </div>
  );
}

export default AccountUnlocker;

Combined Admin Panel - Status + Unlock

import React, { useState, useEffect } from 'react';
import AdminService from '../../services/API/AdminService';

function AccountManager() {
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState(null);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  const [unlocking, setUnlocking] = useState(false);

  const checkStatus = async () => {
    if (!email) return;

    setError('');
    setLoading(true);

    try {
      const accountStatus = await AdminService.getAccountStatus(email);
      setStatus(accountStatus);
    } catch (err) {
      setError(err.message);
      setStatus(null);
    } finally {
      setLoading(false);
    }
  };

  const handleUnlock = async () => {
    if (!confirm(`Unlock account for "${email}"?`)) {
      return;
    }

    setError('');
    setUnlocking(true);

    try {
      await AdminService.unlockAccount(email);
      // Refresh status after unlock
      await checkStatus();
      alert(`Account unlocked successfully for ${email}`);
    } catch (err) {
      setError(err.message);
    } finally {
      setUnlocking(false);
    }
  };

  return (
    <div className="account-manager">
      <h2>Account Management</h2>

      <div className="search-form">
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter email address"
          maxLength={255}
        />
        <button onClick={checkStatus} disabled={loading || !email}>
          {loading ? 'Checking...' : 'Check Status'}
        </button>
      </div>

      {error && <p className="error">{error}</p>}

      {status && (
        <div className="account-info">
          <h3>{status.email}</h3>

          <div className="status-details">
            <p>
              Status:{" "}
              <strong className={status.isLocked ? "text-red" : "text-green"}>
                {status.isLocked ? "🔒 LOCKED" : "✓ Not Locked"}
              </strong>
            </p>
            <p>Failed Attempts: {status.failedAttempts}</p>

            {status.isLocked && (
              <>
                <p>Automatic Unlock In: {status.remainingTime}</p>
                <p className="text-muted">
                  ({status.remainingSeconds} seconds)
                </p>

                <button
                  onClick={handleUnlock}
                  disabled={unlocking}
                  className="btn-danger"
                >
                  {unlocking ? 'Unlocking...' : 'Unlock Account Now'}
                </button>
              </>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

export default AccountManager;

Locked Users Dashboard

import React, { useState, useEffect } from 'react';
import AdminService from '../../services/API/AdminService';

function LockedUsersDashboard({ suspectedEmails }) {
  const [lockedUsers, setLockedUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const checkAllUsers = async () => {
      setLoading(true);
      const locked = [];

      for (const email of suspectedEmails) {
        try {
          const status = await AdminService.getAccountStatus(email);
          if (status.isLocked) {
            locked.push(status);
          }
        } catch (err) {
          console.error(`Failed to check ${email}:`, err);
        }
      }

      setLockedUsers(locked);
      setLoading(false);
    };

    checkAllUsers();
  }, [suspectedEmails]);

  const unlockUser = async (email) => {
    try {
      await AdminService.unlockAccount(email);
      // Remove from locked users list
      setLockedUsers(prev => prev.filter(user => user.email !== email));
    } catch (err) {
      alert(`Failed to unlock ${email}: ${err.message}`);
    }
  };

  if (loading) return <div>Checking locked accounts...</div>;

  if (lockedUsers.length === 0) {
    return <div>No locked accounts found.</div>;
  }

  return (
    <div className="locked-users-dashboard">
      <h2>Locked User Accounts ({lockedUsers.length})</h2>

      <table>
        <thead>
          <tr>
            <th>Email</th>
            <th>Failed Attempts</th>
            <th>Unlocks In</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          {lockedUsers.map(user => (
            <tr key={user.email}>
              <td>{user.email}</td>
              <td>{user.failedAttempts}</td>
              <td>{user.remainingTime}</td>
              <td>
                <button onClick={() => unlockUser(user.email)}>
                  Unlock Now
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default LockedUsersDashboard;

Testing

Test Check Account Status

// In browser console after admin login

import AdminService from './services/API/AdminService';

// Validate email
const validation = AdminService.validateEmail("user@example.com");
console.log("Email valid:", validation.valid);

// Check account status
const status = await AdminService.getAccountStatus("user@example.com");
console.log("Account status:", status);
console.log("Is locked:", status.isLocked);
console.log("Failed attempts:", status.failedAttempts);

if (status.isLocked) {
  console.log("Remaining time:", status.remainingTime);
  console.log("Remaining seconds:", status.remainingSeconds);
}

Test Unlock Account

// Check if needs unlock
const needs = await AdminService.needsUnlock("user@example.com");
console.log("Needs unlock:", needs);

// Unlock account
if (needs) {
  const result = await AdminService.unlockAccount("user@example.com");
  console.log("Unlock result:", result);
  console.log("Success:", result.success);
  console.log("Message:", result.message);
}

Test with curl

# 1. Login as admin and get access token
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@example.com",
    "password": "AdminPass123!"
  }' | jq -r '.access_token')

# 2. Check account status
curl -X GET "http://localhost:8000/api/v1/admin/account-status?email=user@example.com" \
  -H "Authorization: JWT $ACCESS_TOKEN" | jq

# Example response:
# {
#   "email": "user@example.com",
#   "is_locked": true,
#   "failed_attempts": 5,
#   "remaining_time": "5 minutes 30 seconds",
#   "remaining_seconds": 330
# }

# 3. Unlock account
curl -X POST http://localhost:8000/api/v1/admin/unlock-account \
  -H "Content-Type: application/json" \
  -H "Authorization: JWT $ACCESS_TOKEN" \
  -d '{
    "email": "user@example.com"
  }' | jq

# Example response:
# {
#   "success": true,
#   "message": "Account unlocked successfully",
#   "email": "user@example.com"
# }

# 4. Verify account is unlocked
curl -X GET "http://localhost:8000/api/v1/admin/account-status?email=user@example.com" \
  -H "Authorization: JWT $ACCESS_TOKEN" | jq

# Should show:
# {
#   "email": "user@example.com",
#   "is_locked": false,
#   "failed_attempts": 0
# }

Important Notes

Security and Authorization

  1. Admin Role Required: Both endpoints require admin authentication
  2. Security Event Logging: Unlock operations log security events for audit trail
  3. Admin User ID: The admin who performs unlock is logged (from JWT)
  4. Rate Limiting: Generic rate limiting applied to prevent abuse

Account Lockout Mechanism

  • Accounts are locked after excessive failed login attempts (configurable)
  • Lock duration is typically 15-30 minutes (configurable)
  • Failed attempts counter resets after successful login
  • Automatic unlock occurs when lock time expires
  • Admin can unlock immediately without waiting

Integration with CWE-307 Protection

These endpoints are part of the security system that protects against:

  • CWE-307: Improper Restriction of Excessive Authentication Attempts
  • CWE-770: Allocation of Resources Without Limits or Throttling

The login rate limiter tracks:

  • Failed login attempts per email
  • Account lock status and expiry
  • Grace period for automatic unlock

Best Practices

  1. Verify Admin Role: Always check user has admin role before showing UI
  2. Confirm Before Unlock: Always require confirmation before unlocking
  3. Audit Trail: Log all admin actions for security compliance
  4. User Notification: Consider notifying users when their account is unlocked by admin
  5. Regular Review: Periodically review locked accounts dashboard

Created Files

src/services/API/AdminService.js
docs/ADMIN_API.md

Backend Reference Files

cloud/maplepress-backend/internal/interface/http/handler/admin/account_status_handler.go
cloud/maplepress-backend/internal/interface/http/handler/admin/unlock_account_handler.go
cloud/maplepress-backend/internal/interface/http/server.go (routes)

Summary

The Admin API implementation provides:

  1. Account Status Checking: View lock status, failed attempts, and remaining time
  2. Account Unlocking: Manually unlock accounts with security event logging
  3. Helper Functions: Email validation and time formatting
  4. Security Compliance: Admin role enforcement and audit trail
  5. Error Handling: Clear error messages and graceful failures

Essential for managing user account security and providing admin support for locked-out users while maintaining security compliance (CWE-307 protection).


Last Updated: October 30, 2024 Frontend Version: 0.0.0 Documentation Version: 1.0.0