17 KiB
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
{
"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.
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:
{
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.
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.
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.
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
// 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
- JWT Authentication Required: Endpoint requires valid JWT token
- Token Validation: Backend validates token signature and expiration
- No PII in Logs: Backend uses hashed/redacted email in logs (CWE-532)
- Automatic Token Refresh: ApiClient ensures token is valid before request
- Context Extraction: User data extracted from secure JWT context
Usage Examples
Display User Profile in Dashboard
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
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
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
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
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:
// 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
// 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
# 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
// 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
// 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)
// MeService uses ApiClient, which automatically:
// - Adds JWT token to Authorization header
// - Refreshes token if expired
// - Handles 401 errors
Token Refresh (RefreshTokenService)
// 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
// 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.
const profile = await MeService.getMe();
setDashboardHeader(profile.name, profile.email);
2. Role-Based UI
Show or hide UI elements based on user role.
const isAdmin = await MeService.hasRole("owner");
setShowAdminPanel(isAdmin);
3. Tenant Context
Ensure user is operating in the correct tenant context.
const tenantId = await MeService.getTenantId();
loadTenantSpecificData(tenantId);
4. Profile Verification
Verify that the current user matches a resource owner.
const canEdit = await MeService.isCurrentUser(resourceOwnerId);
5. Session Validation
Periodically verify that the session is still valid.
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:
- User not logged in
- Access token expired
- Refresh token expired
- 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:
- Token was refreshed but AuthManager not updated
- Profile was updated on another device
- 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:
- JWT token missing required claims
- Backend not setting context values correctly
- 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:
- Clock skew between client and server
- Token signature invalid
- 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 and JWT token acquisition
- REFRESH_TOKEN_API.md - Automatic token refresh
- HELLO_API.md - Simple authenticated endpoint testing
- FRONTEND_ARCHITECTURE.md - Architecture overview
- README.md - Documentation index
- Backend API Documentation - Complete API reference
Summary
The Me API implementation provides:
- User Profile Access: Get current user's profile from JWT token
- Fast & Efficient: No database queries, data from token claims
- Helper Methods: Role checking, tenant ID, user verification
- Automatic Integration: Works seamlessly with token refresh
- Error Handling: Clear error messages and graceful failures
- 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