Accessing user data
After users authenticate with Login with Funtico, you can access their profile information and TICO balance. This guide shows you how to retrieve and manage user data securely using a backend approach with secure HTTP-only cookies.
Funtico provides access to user data through authenticated API endpoints:
- User Profile - Basic user information (email, username, etc.)
- TICO Balance - Current cryptocurrency balance
Prerequisites
Section titled “Prerequisites”Before accessing user data, ensure you have:
- Valid Access Token - Obtained through Login with Funtico flow
- Required Scopes -
profile,emailfor user info;balance:read,transactions:read,tournaments:read,progression:readfor extended data access - SDK Initialization - Properly configured Funtico SDK
- Backend Setup - Secure token management with HTTP-only cookies
SDK Setup
Section titled “SDK Setup”Initialize the SDK with your auth client credentials:
import { FunticoSDK } from '@pillarex/funtico-sdk';
const sdk = new FunticoSDK({ authClientId: process.env.FUNTICO_AUTH_CLIENT_ID, authClientSecret: process.env.FUNTICO_AUTH_CLIENT_SECRET, env: process.env.NODE_ENV === 'production' ? 'production' : 'staging'});Retrieving User Balance
Section titled “Retrieving User Balance”Backend Endpoint for Balance
Section titled “Backend Endpoint for Balance”Create a secure backend endpoint to get the user’s TICO balance:
// Backend: GET /api/user/balanceapp.get('/api/user/balance', async (req, res) => { try { const accessToken = req.cookies.access_token; if (!accessToken) { return res.status(401).json({ error: 'Not authenticated' }); }
const balance = await sdk.getUserBalance({ accessToken: accessToken });
res.json({ balance: balance, // Returns balance in smallest unit (e.g., 1000 = 10.00 TICO) formattedBalance: (balance / 100).toFixed(2) // Display format }); } catch (error) { console.error('Failed to get user balance:', error); res.status(500).json({ error: 'Failed to retrieve balance' }); }});Frontend Integration
Section titled “Frontend Integration”Your frontend would call the balance endpoint:
// Frontend: Get user balanceasync function getUserBalance() { try { const response = await fetch('/api/user/balance', { credentials: 'include' // Include cookies });
if (response.ok) { const { balance, formattedBalance } = await response.json(); console.log(`User balance: ${formattedBalance} TICO`); return { balance, formattedBalance }; } else { throw new Error('Failed to get balance'); } } catch (error) { console.error('Failed to get user balance:', error); throw error; }}Balance with Error Handling
Section titled “Balance with Error Handling”Implement robust error handling in your backend endpoint:
// Backend: GET /api/user/balance with comprehensive error handlingapp.get('/api/user/balance', async (req, res) => { try { const accessToken = req.cookies.access_token; if (!accessToken) { return res.status(401).json({ error: 'Authentication required. Please log in again.', code: 'AUTH_REQUIRED' }); }
const balance = await sdk.getUserBalance({ accessToken: accessToken });
res.json({ success: true, balance: balance, formattedBalance: (balance / 100).toFixed(2) // Convert to TICO with 2 decimal places }); } catch (error) { if (isSDKError(error)) { switch (error.name) { case 'invalid_token': res.status(401).json({ error: 'Authentication required. Please log in again.', code: 'AUTH_REQUIRED' }); break; case 'insufficient_scope': res.status(403).json({ error: 'Insufficient permissions to access balance.', code: 'INSUFFICIENT_SCOPE' }); break; default: res.status(500).json({ error: 'Failed to retrieve balance. Please try again.', code: 'UNKNOWN_ERROR' }); } } else { res.status(500).json({ error: 'Network error. Please check your connection.', code: 'NETWORK_ERROR' }); } }});Retrieving User Profile
Section titled “Retrieving User Profile”Backend Endpoint for Profile
Section titled “Backend Endpoint for Profile”Create a secure backend endpoint to get user profile information:
// Backend: GET /api/user/profileapp.get('/api/user/profile', async (req, res) => { try { const accessToken = req.cookies.access_token; if (!accessToken) { return res.status(401).json({ error: 'Not authenticated' }); }
const userInfo = await sdk.getUserInfo({ accessToken: accessToken });
res.json({ profile: userInfo }); } catch (error) { console.error('Failed to get user profile:', error); res.status(500).json({ error: 'Failed to retrieve profile' }); }});Complete User Data Endpoint
Section titled “Complete User Data Endpoint”Retrieve both profile and balance in a single backend endpoint:
// Backend: GET /api/user/dataapp.get('/api/user/data', async (req, res) => { try { const accessToken = req.cookies.access_token; if (!accessToken) { return res.status(401).json({ error: 'Not authenticated' }); }
// Fetch profile and balance in parallel const [userInfo, balance] = await Promise.all([ sdk.getUserInfo({ accessToken }), sdk.getUserBalance({ accessToken }) ]);
res.json({ profile: userInfo, balance: balance, formattedBalance: (balance / 100).toFixed(2) }); } catch (error) { console.error('Failed to get user data:', error); res.status(500).json({ error: 'Failed to retrieve user data' }); }});Frontend Integration
Section titled “Frontend Integration”Your frontend would call these endpoints:
// Frontend: Get complete user dataasync function getCompleteUserData() { try { const response = await fetch('/api/user/data', { credentials: 'include' // Include cookies });
if (response.ok) { const userData = await response.json(); console.log(`Welcome, ${userData.profile.name}!`); console.log(`Your balance: ${userData.formattedBalance} TICO`); return userData; } else { throw new Error('Failed to get user data'); } } catch (error) { console.error('Failed to get user data:', error); throw error; }}Data Format and Structure
Section titled “Data Format and Structure”Balance Response
Section titled “Balance Response”The balance endpoint returns:
// Balance response format{ balance: number // Balance in smallest currency unit (e.g., 1000 = 10.00 TICO)}User Profile Response
Section titled “User Profile Response”The user profile contains standard OIDC claims:
// User profile response format{ sub: string, // Unique user identifier email: string, // User's email address email_verified: boolean, // Whether email is verified name: string, // User's display name preferred_username: string, // User's chosen username image: string, // User's avatar image URL frame: string // User's avatar frame URL}Error Handling
Section titled “Error Handling”Common Error Scenarios
Section titled “Common Error Scenarios”// Backend: Handle different error types in user data endpointsapp.get('/api/user/data', requireAuth, async (req, res) => { try { const [userInfo, balance] = await Promise.all([ sdk.getUserInfo({ accessToken: req.accessToken }), sdk.getUserBalance({ accessToken: req.accessToken }) ]);
res.json({ success: true, profile: userInfo, balance }); } catch (error) { if (isSDKError(error)) { switch (error.name) { case 'invalid_token': res.status(401).json({ error: 'Authentication required. Please log in again.', code: 'INVALID_TOKEN' }); break;
case 'insufficient_scope': res.status(403).json({ error: 'Token missing required scope', code: 'INSUFFICIENT_SCOPE' }); break;
case 'rate_limit_exceeded': res.status(429).json({ error: 'Rate limit exceeded, please wait', code: 'RATE_LIMITED' }); break;
default: res.status(500).json({ error: 'Unknown SDK error', code: 'SDK_ERROR' }); } } else { res.status(500).json({ error: 'Network or server error', code: 'SERVER_ERROR' }); } }})User-Friendly Error Messages
Section titled “User-Friendly Error Messages”// Frontend: Handle API errors with user-friendly messagesfunction getErrorMessage(errorCode: string): string { switch (errorCode) { case 'INVALID_TOKEN': case 'NO_TOKEN': return 'Your session has expired. Please log in again.'; case 'INSUFFICIENT_SCOPE': return 'Unable to access this data. Please contact support.'; case 'RATE_LIMITED': return 'Too many requests. Please try again in a moment.'; case 'SDK_ERROR': return 'Unable to load user data. Please try again.'; case 'SERVER_ERROR': return 'Server error. Please check your connection and try again.'; default: return 'An unexpected error occurred. Please try again.'; }}
// Frontend: Usage in UIasync function loadUserData() { try { const response = await fetch('/api/user/data', { credentials: 'include' });
if (response.ok) { const userData = await response.json(); setUserData(userData); } else { const error = await response.json(); const message = getErrorMessage(error.code || 'UNKNOWN'); showErrorNotification(message); } } catch (error) { showErrorNotification('Network error. Please check your connection.'); }}Caching and Performance
Section titled “Caching and Performance”Implement Caching
Section titled “Implement Caching”Cache user data to reduce API calls:
// Simple caching implementationclass UserDataCache { private cache = new Map<string, { data: any; timestamp: number }>(); private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
async getUserBalance(accessToken: string): Promise<number> { const cacheKey = `balance_${accessToken}`; const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) { return cached.data; }
try { const balance = await sdk.getUserBalance({ accessToken });
// Cache the result this.cache.set(cacheKey, { data: balance, timestamp: Date.now() });
return balance; } catch (error) { // Return cached data if available, even if expired if (cached) { console.warn('Using cached balance due to API error'); return cached.data; } throw error; } }
// Clear cache for a specific user clearUserCache(accessToken: string) { const cacheKey = `balance_${accessToken}`; this.cache.delete(cacheKey); }}
// Usageconst userCache = new UserDataCache();const balance = await userCache.getUserBalance(accessToken);Real-time Updates
Section titled “Real-time Updates”Polling for Balance Updates
Section titled “Polling for Balance Updates”Implement polling for real-time balance updates:
// Polling for balance updatesclass BalancePoller { private intervalId: NodeJS.Timeout | null = null; private lastBalance: number | null = null; private onBalanceChange: ((balance: number) => void) | null = null;
startPolling(onBalanceChange: (balance: number) => void) { this.onBalanceChange = onBalanceChange;
this.intervalId = setInterval(async () => { try { const response = await fetch('/api/user/balance', { credentials: 'include' });
if (response.ok) { const { balance } = await response.json();
if (this.lastBalance !== null && this.lastBalance !== balance) { this.onBalanceChange?.(balance); }
this.lastBalance = balance; } } catch (error) { console.error('Failed to poll balance:', error); } }, 30000); // Poll every 30 seconds }
stopPolling() { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } }}
// Usage in React componentfunction BalanceDisplay() { const [balance, setBalance] = useState<number | null>(null); const pollerRef = useRef<BalancePoller | null>(null);
useEffect(() => { pollerRef.current = new BalancePoller(); pollerRef.current.startPolling(setBalance);
return () => { pollerRef.current?.stopPolling(); }; }, []);
return ( <div> {balance !== null ? ( <p>Balance: {(balance / 100).toFixed(2)} TICO</p> ) : ( <p>Loading balance...</p> )} </div> );}Security Best Practices
Section titled “Security Best Practices”Token Validation
Section titled “Token Validation”Backend middleware automatically validates tokens from cookies:
// Backend: Token validation middleware with error handlingfunction requireAuth(req, res, next) { const accessToken = req.cookies.access_token;
if (!accessToken) { return res.status(401).json({ error: 'Authentication required', code: 'NO_TOKEN' }); }
req.accessToken = accessToken; next();}
// Backend: Enhanced validation with token refresh attemptasync function requireAuthWithRefresh(req, res, next) { const accessToken = req.cookies.access_token;
if (!accessToken) { return res.status(401).json({ error: 'Authentication required', code: 'NO_TOKEN' }); }
try { // Validate token by making a test call await sdk.getUserInfo({ accessToken }); req.accessToken = accessToken; next(); } catch (error) { if (isSDKError(error) && error.name === 'invalid_token') { return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' }); } throw error; }}Secure Token Storage
Section titled “Secure Token Storage”Store tokens securely using HTTP-only cookies on the backend:
// Backend: Secure token managementapp.post('/auth/refresh', async (req, res) => { try { const refreshToken = req.cookies.refresh_token; if (!refreshToken) { return res.status(401).json({ error: 'No refresh token' }); }
const { accessToken, refreshToken: newRefreshToken } = await sdk.refreshTokens({ refreshToken });
// Update tokens in secure cookies res.cookie('access_token', accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 1000 // 1 hour });
res.cookie('refresh_token', newRefreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days });
res.json({ success: true }); } catch (error) { res.status(401).json({ error: 'Failed to refresh token' }); }});
// Backend: Token validation middlewarefunction requireAuth(req, res, next) { const accessToken = req.cookies.access_token; if (!accessToken) { return res.status(401).json({ error: 'Authentication required' }); } req.accessToken = accessToken; next();}
// Usageapp.get('/api/user/balance', requireAuth, async (req, res) => { try { const balance = await sdk.getUserBalance({ accessToken: req.accessToken }); res.json({ balance }); } catch (error) { res.status(500).json({ error: 'Failed to get balance' }); }});Complete Example
Section titled “Complete Example”Here’s a complete example with backend endpoints and frontend integration:
// Complete backend user data serviceimport express from 'express';import cookieParser from 'cookie-parser';import { FunticoSDK } from '@pillarex/funtico-sdk';
const app = express();app.use(cookieParser());
const sdk = new FunticoSDK({ authClientId: process.env.FUNTICO_AUTH_CLIENT_ID!, authClientSecret: process.env.FUNTICO_AUTH_CLIENT_SECRET!, env: 'staging'});
// In-memory cache (use Redis in production)const cache = new Map();const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
// Authentication middlewarefunction requireAuth(req, res, next) { const accessToken = req.cookies.access_token; if (!accessToken) { return res.status(401).json({ error: 'Authentication required' }); } req.accessToken = accessToken; next();}
// Get complete user data with cachingapp.get('/api/user/data', requireAuth, async (req, res) => { try { const cacheKey = `user_data_${req.accessToken}`; const cached = cache.get(cacheKey);
// Return cached data if still valid if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return res.json(cached.data); }
// Fetch fresh data const [userInfo, balance] = await Promise.all([ sdk.getUserInfo({ accessToken: req.accessToken }), sdk.getUserBalance({ accessToken: req.accessToken }) ]);
const userData = { profile: userInfo, balance: balance, formattedBalance: (balance / 100).toFixed(2), lastUpdated: new Date().toISOString() };
// Cache the result cache.set(cacheKey, { data: userData, timestamp: Date.now() });
res.json(userData); } catch (error) { console.error('Failed to get user data:', error); res.status(500).json({ error: 'Failed to retrieve user data' }); }});
// Refresh user data (bypass cache)app.post('/api/user/data/refresh', requireAuth, async (req, res) => { try { const cacheKey = `user_data_${req.accessToken}`; cache.delete(cacheKey); // Clear cache
// Fetch fresh data const [userInfo, balance] = await Promise.all([ sdk.getUserInfo({ accessToken: req.accessToken }), sdk.getUserBalance({ accessToken: req.accessToken }) ]);
const userData = { profile: userInfo, balance: balance, formattedBalance: (balance / 100).toFixed(2), lastUpdated: new Date().toISOString() };
res.json(userData); } catch (error) { console.error('Failed to refresh user data:', error); res.status(500).json({ error: 'Failed to refresh user data' }); }});// Frontend user data serviceclass UserDataService { async getUserData() { try { const response = await fetch('/api/user/data', { credentials: 'include' });
if (response.ok) { const userData = await response.json(); console.log(`Welcome, ${userData.profile.name}!`); console.log(`Balance: ${userData.formattedBalance} TICO`); return userData; } else { throw new Error('Failed to get user data'); } } catch (error) { console.error('Failed to get user data:', error); throw error; } }
async refreshUserData() { try { const response = await fetch('/api/user/data/refresh', { method: 'POST', credentials: 'include' });
if (response.ok) { return await response.json(); } else { throw new Error('Failed to refresh user data'); } } catch (error) { console.error('Failed to refresh user data:', error); throw error; } }}
// Usageconst userService = new UserDataService();
// Get user dataconst userData = await userService.getUserData();
// Refresh dataconst freshData = await userService.refreshUserData();Next Steps
Section titled “Next Steps”- Login Integration - Set up user authentication
- Payment Integration - Integrate payments with user data