- Add reusable AuthGuard component for protected routes - Move auth logic from index.tsx to (tabs)/_layout.tsx via AuthGuard - Fix ApiClient to handle empty responses (204 No Content) - Use useFocusEffect in chat.tsx to load messages after auth is ready - Extract getDateKey() helper function in calendar.tsx
104 lines
2.5 KiB
TypeScript
104 lines
2.5 KiB
TypeScript
import { Platform } from "react-native";
|
|
import { apiLogger } from "../logging";
|
|
import { useAuthStore } from "../stores";
|
|
|
|
const API_BASE_URL =
|
|
process.env.EXPO_PUBLIC_API_URL ||
|
|
Platform.select({
|
|
android: "http://10.0.2.2:3001/api",
|
|
default: "http://localhost:3001/api",
|
|
});
|
|
|
|
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
|
|
interface RequestOptions {
|
|
headers?: Record<string, string>;
|
|
body?: unknown;
|
|
skipAuth?: boolean;
|
|
}
|
|
|
|
function getAuthHeaders(): Record<string, string> {
|
|
const user = useAuthStore.getState().user;
|
|
apiLogger.debug(`getAuthHeaders - user: ${JSON.stringify(user)}`);
|
|
if (user?.id) {
|
|
return { "X-User-Id": user.id };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
async function request<T>(
|
|
method: HttpMethod,
|
|
endpoint: string,
|
|
options?: RequestOptions,
|
|
): Promise<T> {
|
|
const start = performance.now();
|
|
apiLogger.debug(`${method} ${endpoint}`);
|
|
|
|
try {
|
|
const authHeaders = options?.skipAuth ? {} : getAuthHeaders();
|
|
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
method,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...authHeaders,
|
|
...options?.headers,
|
|
},
|
|
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
});
|
|
|
|
const duration = Math.round(performance.now() - start);
|
|
|
|
if (!response.ok) {
|
|
apiLogger.error(`${method} ${endpoint} - ${response.status} (${duration}ms)`);
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
|
|
apiLogger.debug(`${method} ${endpoint} - ${response.status} (${duration}ms)`);
|
|
|
|
const text = await response.text();
|
|
if (!text) {
|
|
return undefined as T;
|
|
}
|
|
return JSON.parse(text);
|
|
} catch (error) {
|
|
const duration = Math.round(performance.now() - start);
|
|
apiLogger.error(`${method} ${endpoint} failed (${duration}ms): ${error}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export const ApiClient = {
|
|
get: async <T>(
|
|
endpoint: string,
|
|
options?: Omit<RequestOptions, "body">,
|
|
): Promise<T> => {
|
|
return request<T>("GET", endpoint, options);
|
|
},
|
|
|
|
post: async <T>(
|
|
endpoint: string,
|
|
body?: unknown,
|
|
options?: RequestOptions,
|
|
): Promise<T> => {
|
|
return request<T>("POST", endpoint, { ...options, body });
|
|
},
|
|
|
|
put: async <T>(
|
|
endpoint: string,
|
|
body?: unknown,
|
|
options?: RequestOptions,
|
|
): Promise<T> => {
|
|
return request<T>("PUT", endpoint, { ...options, body });
|
|
},
|
|
|
|
delete: async <T>(
|
|
endpoint: string,
|
|
options?: Omit<RequestOptions, "body">,
|
|
): Promise<T> => {
|
|
return request<T>("DELETE", endpoint, options);
|
|
},
|
|
};
|
|
|
|
export { API_BASE_URL };
|