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.gocloud/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/jsonAuthorization: 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
- Admin Role Required: Both endpoints require admin authentication
- Security Event Logging: Unlock operations log security events for audit trail
- Admin User ID: The admin who performs unlock is logged (from JWT)
- 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
- Verify Admin Role: Always check user has admin role before showing UI
- Confirm Before Unlock: Always require confirmation before unlocking
- Audit Trail: Log all admin actions for security compliance
- User Notification: Consider notifying users when their account is unlocked by admin
- Regular Review: Periodically review locked accounts dashboard
Related Files
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)
Related Documentation
- LOGIN_API.md - User login with rate limiting
- ME_API.md - Current user profile includes role checking
- USER_API.md - User management operations
- FRONTEND_ARCHITECTURE.md - Architecture overview
- README.md - Documentation index
Summary
The Admin API implementation provides:
- Account Status Checking: View lock status, failed attempts, and remaining time
- Account Unlocking: Manually unlock accounts with security event logging
- Helper Functions: Email validation and time formatting
- Security Compliance: Admin role enforcement and audit trail
- 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