Files
PoleDanceApp/mobile/src/api/client.ts
Dianaka123 789d2bf0a6 Full app rebuild: FastAPI backend + React Native mobile with auth, championships, admin
Backend (FastAPI + SQLAlchemy + SQLite):
- JWT auth with access/refresh tokens, bcrypt password hashing
- User model with member/organizer/admin roles, auto-approve members
- Championship, Registration, ParticipantList, Notification models
- Alembic async migrations, seed data with test users
- Registration endpoint returns tokens for members, pending for organizers
- /registrations/my returns championship title/date/location via eager loading
- Admin endpoints: list users, approve/reject organizers

Mobile (React Native + Expo + TypeScript):
- Zustand auth store, Axios client with token refresh interceptor
- Role-based registration (Member vs Organizer) with contextual form labels
- Tab navigation with Ionicons, safe area headers, admin tab for admin role
- Championships list with status badges, detail screen with registration progress
- My Registrations with championship title, progress bar, and tap-to-navigate
- Admin panel with pending/all filter, approve/reject with confirmation
- Profile screen with role badge, Ionicons info rows, sign out
- Password visibility toggle (Ionicons), keyboard flow hints (returnKeyType)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:46:50 +03:00

74 lines
2.2 KiB
TypeScript

import axios from 'axios';
import { tokenStorage } from '../utils/tokenStorage';
// Replace with your machine's LAN IP when testing on a physical device
export const BASE_URL = 'http://192.168.2.56:8000/api/v1';
export const apiClient = axios.create({
baseURL: BASE_URL,
timeout: 10000,
});
// Attach access token from in-memory cache (synchronous — no await needed)
apiClient.interceptors.request.use((config) => {
const token = tokenStorage.getAccessTokenSync();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Refresh token on 401
let isRefreshing = false;
let queue: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void }> = [];
function processQueue(error: unknown, token: string | null = null) {
queue.forEach((p) => (error ? p.reject(error) : p.resolve(token!)));
queue = [];
}
apiClient.interceptors.response.use(
(res) => res,
async (error) => {
const original = error.config;
if (error.response?.status === 401 && !original._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
queue.push({
resolve: (token) => {
original.headers.Authorization = `Bearer ${token}`;
resolve(apiClient(original));
},
reject,
});
});
}
original._retry = true;
isRefreshing = true;
try {
const refreshToken = tokenStorage.getRefreshTokenSync();
if (!refreshToken) throw new Error('No refresh token');
const { data } = await axios.post(`${BASE_URL}/auth/refresh`, {
refresh_token: refreshToken,
});
await tokenStorage.saveTokens(data.access_token, data.refresh_token);
apiClient.defaults.headers.common.Authorization = `Bearer ${data.access_token}`;
processQueue(null, data.access_token);
original.headers.Authorization = `Bearer ${data.access_token}`;
return apiClient(original);
} catch (err) {
processQueue(err, null);
await tokenStorage.clearTokens();
return Promise.reject(err);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);