Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,827 @@
# 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
```json
{
"email": "user@example.com",
"is_locked": true,
"failed_attempts": 5,
"remaining_time": "5 minutes 30 seconds",
"remaining_seconds": 330
}
```
**When Account Not Locked:**
```json
{
"email": "user@example.com",
"is_locked": false,
"failed_attempts": 2
}
```
### Unlock Account Request
```json
{
"email": "user@example.com"
}
```
**Headers Required:**
- `Content-Type: application/json`
- `Authorization: JWT {access_token}` (admin role required)
### Unlock Account Response
```json
{
"success": true,
"message": "Account unlocked successfully",
"email": "user@example.com"
}
```
**When Account Not Locked:**
```json
{
"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.
```javascript
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:**
```javascript
{
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.
```javascript
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:**
```javascript
{
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).
```javascript
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.
```javascript
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.
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
// 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
```javascript
// 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
```bash
# 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
## 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](./LOGIN_API.md) - User login with rate limiting
- [ME_API.md](./ME_API.md) - Current user profile includes role checking
- [USER_API.md](./USER_API.md) - User management operations
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview
- [README.md](./README.md) - Documentation index
## 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