676 lines
17 KiB
Markdown
676 lines
17 KiB
Markdown
# 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
|