Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
676
web/maplepress-frontend/docs/API/ME_API.md
Normal file
676
web/maplepress-frontend/docs/API/ME_API.md
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
# Me API Implementation (User Profile)
|
||||
|
||||
This document describes the implementation of the Me API endpoint for the MaplePress frontend, integrated with the MaplePress backend API.
|
||||
|
||||
## Overview
|
||||
|
||||
The Me API endpoint returns the authenticated user's profile information extracted directly from the JWT token. This is a lightweight endpoint that requires no database queries, making it ideal for displaying user information in the dashboard and verifying the current user's identity.
|
||||
|
||||
## Backend API Endpoint
|
||||
|
||||
**Endpoint**: `GET /api/v1/me`
|
||||
**Authentication**: Required (JWT token)
|
||||
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 374-413)
|
||||
|
||||
### Request Structure
|
||||
|
||||
No request body required. Authentication is provided via the Authorization header:
|
||||
|
||||
**Headers Required:**
|
||||
- `Authorization: JWT {access_token}`
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "john@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "owner",
|
||||
"tenant_id": "650e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
- **No Database Query**: All data extracted from JWT token claims
|
||||
- **Fast Response**: Minimal processing required
|
||||
- **Current User Only**: Returns data for the authenticated user
|
||||
- **Role Information**: Includes user's role for authorization checks
|
||||
- **Tenant Context**: Includes tenant ID for multi-tenant operations
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### MeService (`src/services/API/MeService.js`)
|
||||
|
||||
Handles direct communication with the backend Me API and provides utility methods for user profile operations.
|
||||
|
||||
**Key Features:**
|
||||
- GET request with automatic JWT authentication
|
||||
- Response transformation (snake_case to camelCase)
|
||||
- User-friendly error message mapping
|
||||
- Helper methods for common use cases
|
||||
- Automatic token refresh integration
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `getMe()`
|
||||
Main method to fetch the current user's profile.
|
||||
|
||||
```javascript
|
||||
import MeService from './services/API/MeService';
|
||||
|
||||
const profile = await MeService.getMe();
|
||||
console.log(profile);
|
||||
// Output:
|
||||
// {
|
||||
// userId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
// email: "john@example.com",
|
||||
// name: "John Doe",
|
||||
// role: "owner",
|
||||
// tenantId: "650e8400-e29b-41d4-a716-446655440000"
|
||||
// }
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
userId: string, // User's unique identifier (UUID)
|
||||
email: string, // User's email address
|
||||
name: string, // User's full name
|
||||
role: string, // User's role (e.g., "owner", "admin", "user")
|
||||
tenantId: string // User's tenant/organization ID (UUID)
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- "Authentication required. Please log in to continue." - If JWT token is missing/invalid
|
||||
- "Invalid or expired authentication token. Please log in again." - If token has expired
|
||||
- Generic error message for other failures
|
||||
|
||||
#### `hasRole(requiredRole)`
|
||||
Check if the current user has a specific role.
|
||||
|
||||
```javascript
|
||||
const isOwner = await MeService.hasRole("owner");
|
||||
if (isOwner) {
|
||||
console.log("User is an owner");
|
||||
}
|
||||
|
||||
const isAdmin = await MeService.hasRole("admin");
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `requiredRole` (string): Role to check for (e.g., "owner", "admin", "user")
|
||||
|
||||
**Returns:** `boolean` - True if user has the required role
|
||||
|
||||
#### `getTenantId()`
|
||||
Get the current user's tenant ID.
|
||||
|
||||
```javascript
|
||||
const tenantId = await MeService.getTenantId();
|
||||
console.log("User's tenant:", tenantId);
|
||||
```
|
||||
|
||||
**Returns:** `string|null` - Tenant ID or null if not available
|
||||
|
||||
#### `isCurrentUser(userId)`
|
||||
Verify that the current user matches a specific user ID.
|
||||
|
||||
```javascript
|
||||
const isSameUser = await MeService.isCurrentUser("550e8400-...");
|
||||
if (isSameUser) {
|
||||
console.log("This is the current user");
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `userId` (string): User ID to verify against
|
||||
|
||||
**Returns:** `boolean` - True if the current user matches the provided ID
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User is authenticated (has JWT token)
|
||||
↓
|
||||
Component calls MeService.getMe()
|
||||
↓
|
||||
ApiClient.get("/api/v1/me")
|
||||
↓
|
||||
Token automatically refreshed if needed (ApiClient feature)
|
||||
↓
|
||||
GET /api/v1/me with Authorization header
|
||||
↓
|
||||
Backend JWT middleware extracts user from token
|
||||
↓
|
||||
Backend returns user profile from JWT claims
|
||||
↓
|
||||
MeService transforms response (snake_case → camelCase)
|
||||
↓
|
||||
Component receives user profile
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
| Error Condition | Response | Frontend Behavior |
|
||||
|----------------|----------|-------------------|
|
||||
| Missing JWT token | 401 Unauthorized | "Authentication required. Please log in to continue." |
|
||||
| Invalid JWT token | 401 Unauthorized | "Invalid or expired authentication token." |
|
||||
| Expired JWT token | 401 Unauthorized | Token auto-refresh triggered, then retry |
|
||||
| Server error | 500 Internal Server Error | Generic error message |
|
||||
|
||||
### Error Message Mapping
|
||||
|
||||
```javascript
|
||||
// Backend error → Frontend error
|
||||
"unauthorized" → "Authentication required. Please log in to continue."
|
||||
"token" → "Invalid or expired authentication token. Please log in again."
|
||||
Other errors → Original error message or generic fallback
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **JWT Authentication Required**: Endpoint requires valid JWT token
|
||||
2. **Token Validation**: Backend validates token signature and expiration
|
||||
3. **No PII in Logs**: Backend uses hashed/redacted email in logs (CWE-532)
|
||||
4. **Automatic Token Refresh**: ApiClient ensures token is valid before request
|
||||
5. **Context Extraction**: User data extracted from secure JWT context
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Display User Profile in Dashboard
|
||||
|
||||
```javascript
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function UserProfile() {
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const data = await MeService.getMe();
|
||||
setProfile(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProfile();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div>Loading profile...</div>;
|
||||
if (error) return <div>Error: {error}</div>;
|
||||
if (!profile) return null;
|
||||
|
||||
return (
|
||||
<div className="user-profile">
|
||||
<h2>Welcome, {profile.name}!</h2>
|
||||
<p>Email: {profile.email}</p>
|
||||
<p>Role: {profile.role}</p>
|
||||
<p>User ID: {profile.userId}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserProfile;
|
||||
```
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
```javascript
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function AdminPanel() {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAdmin = async () => {
|
||||
const hasAdminRole = await MeService.hasRole("owner");
|
||||
setIsAdmin(hasAdminRole);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkAdmin();
|
||||
}, []);
|
||||
|
||||
if (loading) return <div>Checking permissions...</div>;
|
||||
|
||||
if (!isAdmin) {
|
||||
return <div>Access denied. Admin rights required.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-panel">
|
||||
<h2>Admin Panel</h2>
|
||||
{/* Admin content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminPanel;
|
||||
```
|
||||
|
||||
### Sync Profile with AuthManager
|
||||
|
||||
```javascript
|
||||
import { useAuth } from '../../services/Services';
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function Dashboard() {
|
||||
const { authManager } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const syncProfile = async () => {
|
||||
try {
|
||||
// Get fresh profile from backend
|
||||
const profile = await MeService.getMe();
|
||||
|
||||
// Get stored profile from AuthManager
|
||||
const storedUser = authManager.getUser();
|
||||
|
||||
// Check if data is in sync
|
||||
if (storedUser.email !== profile.email) {
|
||||
console.warn("Profile data mismatch detected");
|
||||
// Could update AuthManager or prompt user
|
||||
}
|
||||
|
||||
console.log("Profile synced successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to sync profile:", error);
|
||||
}
|
||||
};
|
||||
|
||||
syncProfile();
|
||||
}, [authManager]);
|
||||
|
||||
return <div>Dashboard</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Check Tenant Context
|
||||
|
||||
```javascript
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
async function verifyTenantAccess(requiredTenantId) {
|
||||
const userTenantId = await MeService.getTenantId();
|
||||
|
||||
if (userTenantId !== requiredTenantId) {
|
||||
throw new Error("Access denied. Wrong tenant context.");
|
||||
}
|
||||
|
||||
console.log("Tenant access verified");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Usage
|
||||
try {
|
||||
await verifyTenantAccess("650e8400-e29b-41d4-a716-446655440000");
|
||||
// Proceed with tenant-specific operations
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
// Redirect or show error
|
||||
}
|
||||
```
|
||||
|
||||
### Verify Current User
|
||||
|
||||
```javascript
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
function EditProfileButton({ profileOwnerId }) {
|
||||
const [canEdit, setCanEdit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOwnership = async () => {
|
||||
const isOwner = await MeService.isCurrentUser(profileOwnerId);
|
||||
setCanEdit(isOwner);
|
||||
};
|
||||
|
||||
checkOwnership();
|
||||
}, [profileOwnerId]);
|
||||
|
||||
if (!canEdit) return null;
|
||||
|
||||
return <button>Edit Profile</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing the Me API
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- Backend running at `http://localhost:8000`
|
||||
- Frontend running at `http://localhost:5173`
|
||||
- User logged in (valid JWT token)
|
||||
|
||||
### 2. Testing via Dashboard
|
||||
|
||||
Add a profile component to the dashboard:
|
||||
|
||||
```javascript
|
||||
// In Dashboard.jsx
|
||||
import MeService from '../../services/API/MeService';
|
||||
|
||||
const [userProfile, setUserProfile] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
try {
|
||||
const profile = await MeService.getMe();
|
||||
setUserProfile(profile);
|
||||
console.log("Profile loaded:", profile);
|
||||
} catch (error) {
|
||||
console.error("Failed to load profile:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
}, []);
|
||||
|
||||
// In JSX
|
||||
{userProfile && (
|
||||
<div>
|
||||
<h3>{userProfile.name}</h3>
|
||||
<p>{userProfile.email}</p>
|
||||
<p>Role: {userProfile.role}</p>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### 3. Testing via Browser Console
|
||||
|
||||
```javascript
|
||||
// After logging in, open browser console
|
||||
|
||||
// Test basic profile fetch
|
||||
const profile = await MeService.getMe();
|
||||
console.log(profile);
|
||||
// Expected: User profile object
|
||||
|
||||
// Test role check
|
||||
const isOwner = await MeService.hasRole("owner");
|
||||
console.log("Is owner:", isOwner);
|
||||
|
||||
// Test tenant ID
|
||||
const tenantId = await MeService.getTenantId();
|
||||
console.log("Tenant ID:", tenantId);
|
||||
|
||||
// Test user verification
|
||||
const isCurrent = await MeService.isCurrentUser(profile.userId);
|
||||
console.log("Is current user:", isCurrent); // Should be true
|
||||
```
|
||||
|
||||
### 4. Testing with curl
|
||||
|
||||
```bash
|
||||
# 1. Login and get access token
|
||||
ACCESS_TOKEN=$(curl -X POST http://localhost:8000/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}' | jq -r '.access_token')
|
||||
|
||||
# 2. Test me endpoint
|
||||
curl -X GET http://localhost:8000/api/v1/me \
|
||||
-H "Authorization: JWT $ACCESS_TOKEN" | jq
|
||||
|
||||
# Expected output:
|
||||
# {
|
||||
# "user_id": "550e8400-...",
|
||||
# "email": "test@example.com",
|
||||
# "name": "Test User",
|
||||
# "role": "owner",
|
||||
# "tenant_id": "650e8400-..."
|
||||
# }
|
||||
|
||||
# 3. Test without authentication (should fail)
|
||||
curl -X GET http://localhost:8000/api/v1/me | jq
|
||||
|
||||
# Expected: 401 Unauthorized
|
||||
|
||||
# 4. Test with invalid token (should fail)
|
||||
curl -X GET http://localhost:8000/api/v1/me \
|
||||
-H "Authorization: JWT invalid_token" | jq
|
||||
|
||||
# Expected: 401 Unauthorized
|
||||
```
|
||||
|
||||
### 5. Testing Token Refresh Integration
|
||||
|
||||
```javascript
|
||||
// Test that token refresh works automatically
|
||||
|
||||
// 1. Login
|
||||
// 2. Manually set token expiry to 30 seconds from now
|
||||
const newExpiry = new Date(Date.now() + 30000).toISOString();
|
||||
localStorage.setItem('maplepress_access_expiry', newExpiry);
|
||||
|
||||
// 3. Wait 31 seconds, then call getMe
|
||||
await new Promise(resolve => setTimeout(resolve, 31000));
|
||||
const profile = await MeService.getMe();
|
||||
|
||||
// 4. Check console logs - should see automatic token refresh
|
||||
// 5. Response should be successful despite expired token
|
||||
console.log("Profile after token refresh:", profile);
|
||||
```
|
||||
|
||||
### 6. Testing Against Stored Data
|
||||
|
||||
```javascript
|
||||
// Compare Me API response with stored AuthManager data
|
||||
|
||||
import { useAuth } from './services/Services';
|
||||
import MeService from './services/API/MeService';
|
||||
|
||||
const { authManager } = useAuth();
|
||||
|
||||
// Get stored user
|
||||
const storedUser = authManager.getUser();
|
||||
console.log("Stored user:", storedUser);
|
||||
|
||||
// Get fresh profile from API
|
||||
const profile = await MeService.getMe();
|
||||
console.log("API profile:", profile);
|
||||
|
||||
// Compare
|
||||
console.log("Email match:", storedUser.email === profile.email);
|
||||
console.log("Name match:", storedUser.name === profile.name);
|
||||
console.log("Role match:", storedUser.role === profile.role);
|
||||
```
|
||||
|
||||
## Integration with Existing Services
|
||||
|
||||
The MeService automatically integrates with existing infrastructure:
|
||||
|
||||
### Authentication (AuthManager)
|
||||
```javascript
|
||||
// MeService uses ApiClient, which automatically:
|
||||
// - Adds JWT token to Authorization header
|
||||
// - Refreshes token if expired
|
||||
// - Handles 401 errors
|
||||
```
|
||||
|
||||
### Token Refresh (RefreshTokenService)
|
||||
```javascript
|
||||
// Automatic token refresh before getMe request
|
||||
// If token expires within 1 minute, it's refreshed proactively
|
||||
// If 401 received, token is refreshed and request is retried
|
||||
```
|
||||
|
||||
### Data Consistency
|
||||
```javascript
|
||||
// Me API returns data from JWT token
|
||||
// AuthManager stores data from login/register/refresh
|
||||
// Both should be in sync, but Me API is source of truth
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Dashboard User Display
|
||||
Display the current user's name and email in the dashboard header.
|
||||
|
||||
```javascript
|
||||
const profile = await MeService.getMe();
|
||||
setDashboardHeader(profile.name, profile.email);
|
||||
```
|
||||
|
||||
### 2. Role-Based UI
|
||||
Show or hide UI elements based on user role.
|
||||
|
||||
```javascript
|
||||
const isAdmin = await MeService.hasRole("owner");
|
||||
setShowAdminPanel(isAdmin);
|
||||
```
|
||||
|
||||
### 3. Tenant Context
|
||||
Ensure user is operating in the correct tenant context.
|
||||
|
||||
```javascript
|
||||
const tenantId = await MeService.getTenantId();
|
||||
loadTenantSpecificData(tenantId);
|
||||
```
|
||||
|
||||
### 4. Profile Verification
|
||||
Verify that the current user matches a resource owner.
|
||||
|
||||
```javascript
|
||||
const canEdit = await MeService.isCurrentUser(resourceOwnerId);
|
||||
```
|
||||
|
||||
### 5. Session Validation
|
||||
Periodically verify that the session is still valid.
|
||||
|
||||
```javascript
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await MeService.getMe();
|
||||
console.log("Session still valid");
|
||||
} catch (error) {
|
||||
console.log("Session expired, logging out");
|
||||
authManager.logout();
|
||||
}
|
||||
}, 5 * 60 * 1000); // Every 5 minutes
|
||||
```
|
||||
|
||||
## Data Comparison: JWT vs Stored
|
||||
|
||||
### JWT Token Data (from Me API)
|
||||
- **Source**: JWT token claims
|
||||
- **Updated**: On login, registration, or token refresh
|
||||
- **Authoritative**: Yes (always current)
|
||||
- **Requires Network**: Yes
|
||||
|
||||
### AuthManager Stored Data
|
||||
- **Source**: localStorage (from login/register/refresh responses)
|
||||
- **Updated**: On login, registration, or token refresh
|
||||
- **Authoritative**: No (could be stale)
|
||||
- **Requires Network**: No
|
||||
|
||||
**Recommendation**: Use Me API when you need authoritative current data, use AuthManager for quick access without network calls.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication required" error
|
||||
|
||||
**Possible causes:**
|
||||
1. User not logged in
|
||||
2. Access token expired
|
||||
3. Refresh token expired
|
||||
4. Session invalidated
|
||||
|
||||
**Solution:**
|
||||
- Check `authManager.isAuthenticated()` before calling
|
||||
- Login again if session expired
|
||||
- Check browser console for token refresh logs
|
||||
|
||||
### Profile data doesn't match stored data
|
||||
|
||||
**Possible causes:**
|
||||
1. Token was refreshed but AuthManager not updated
|
||||
2. Profile was updated on another device
|
||||
3. Data corruption in localStorage
|
||||
|
||||
**Solution:**
|
||||
- Me API is source of truth - use its data
|
||||
- Update AuthManager with fresh data if needed
|
||||
- Clear localStorage and login again if corrupted
|
||||
|
||||
### Request succeeds but returns empty fields
|
||||
|
||||
**Possible causes:**
|
||||
1. JWT token missing required claims
|
||||
2. Backend not setting context values correctly
|
||||
3. Token format incorrect
|
||||
|
||||
**Solution:**
|
||||
- Check token structure (decode JWT at jwt.io)
|
||||
- Verify backend is setting all required claims
|
||||
- Re-login to get fresh token
|
||||
|
||||
### 401 error despite valid token
|
||||
|
||||
**Possible causes:**
|
||||
1. Clock skew between client and server
|
||||
2. Token signature invalid
|
||||
3. Token expired but not detected
|
||||
|
||||
**Solution:**
|
||||
- Check system clock synchronization
|
||||
- Clear tokens and login again
|
||||
- Verify token expiry dates in localStorage
|
||||
|
||||
## Related Files
|
||||
|
||||
### Created Files
|
||||
```
|
||||
src/services/API/MeService.js
|
||||
docs/ME_API.md
|
||||
```
|
||||
|
||||
### Backend Reference Files
|
||||
```
|
||||
cloud/maplepress-backend/docs/API.md (lines 374-413)
|
||||
cloud/maplepress-backend/internal/interface/http/handler/gateway/me_handler.go
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [LOGIN_API.md](./LOGIN_API.md) - Login and JWT token acquisition
|
||||
- [REFRESH_TOKEN_API.md](./REFRESH_TOKEN_API.md) - Automatic token refresh
|
||||
- [HELLO_API.md](./HELLO_API.md) - Simple authenticated endpoint testing
|
||||
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Architecture overview
|
||||
- [README.md](./README.md) - Documentation index
|
||||
- [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference
|
||||
|
||||
## Summary
|
||||
|
||||
The Me API implementation provides:
|
||||
|
||||
1. **User Profile Access**: Get current user's profile from JWT token
|
||||
2. **Fast & Efficient**: No database queries, data from token claims
|
||||
3. **Helper Methods**: Role checking, tenant ID, user verification
|
||||
4. **Automatic Integration**: Works seamlessly with token refresh
|
||||
5. **Error Handling**: Clear error messages and graceful failures
|
||||
6. **Use Case Focused**: Methods designed for common scenarios
|
||||
|
||||
This endpoint is essential for displaying user information in the dashboard and implementing role-based access control.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 30, 2024
|
||||
**Frontend Version**: 0.0.0
|
||||
**Documentation Version**: 1.0.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue