chore: durcit la configuration pour la production

- mobile: externalise l'URL d'API via EXPO_PUBLIC_API_URL (repli localhost en dev)
- backend: .env.example adapté au projet (PostgreSQL, JWT, FRONTEND_URL, Redis)
- frontend/mobile: ajoute .env.example (+ exception gitignore front)
- mobile: supprime les 4 `any` (helper getApiError, type Feed, glyphMap Ionicons)
- backend: normalise le style PSR-12 via Pint (13 fichiers)

Tests backend 15/15, mobile tsc 0, Pint pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Azmog
2026-06-17 17:00:33 +02:00
parent 052e2f22cd
commit 6f5b8182ca
22 changed files with 122 additions and 95 deletions
+5
View File
@@ -0,0 +1,5 @@
# URL de l'API REST CESIZen utilisée par l'application mobile.
# Préfixe EXPO_PUBLIC_ requis pour être exposée au bundle Expo.
# En production, pointer vers l'API HTTPS (ex. https://api.cesizen.fr/api/v1).
# En dev, laisser vide : l'app retombe sur localhost / 10.0.2.2 selon la plateforme.
EXPO_PUBLIC_API_URL=
+2 -1
View File
@@ -3,12 +3,13 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useFeeds } from '@/hooks/useFeeds';
import { Colors } from '@/lib/colors';
import type { Feed } from '@/types';
export default function InfoListScreen() {
const { data: feeds, isLoading } = useFeeds();
const router = useRouter();
const renderItem = ({ item }: { item: any }) => (
const renderItem = ({ item }: { item: Feed }) => (
<TouchableOpacity
style={styles.card}
onPress={() => router.push(`/(tabs)/info/${item.slug}`)}
+2 -2
View File
@@ -67,10 +67,10 @@ export default function ProfileScreen() {
);
}
function MenuItem({ icon, label }: { icon: string; label: string }) {
function MenuItem({ icon, label }: { icon: keyof typeof Ionicons.glyphMap; label: string }) {
return (
<TouchableOpacity style={styles.menuItem}>
<Ionicons name={icon as any} size={20} color={Colors.gray[600]} />
<Ionicons name={icon} size={20} color={Colors.gray[600]} />
<Text style={styles.menuLabel}>{label}</Text>
<Ionicons name="chevron-forward" size={18} color={Colors.gray[300]} />
</TouchableOpacity>
+3 -2
View File
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Link, useRouter } from 'expo-router';
import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors';
export default function LoginScreen() {
@@ -20,8 +21,8 @@ export default function LoginScreen() {
try {
await login(email, password);
router.replace('/(tabs)');
} catch (error: any) {
Alert.alert('Erreur', error.response?.data?.message || 'Identifiants incorrects');
} catch (error) {
Alert.alert('Erreur', getApiError(error).message || 'Identifiants incorrects');
} finally {
setLoading(false);
}
+5 -3
View File
@@ -3,6 +3,7 @@ import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvo
import { Link, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -43,9 +44,10 @@ export default function RegisterScreen() {
consentement_rgpd: consentement,
});
router.replace('/(tabs)');
} catch (error: any) {
const msg = error.response?.data?.message || 'Erreur lors de l\'inscription';
const apiErrors = error.response?.data?.errors;
} catch (error) {
const data = getApiError(error);
const msg = data.message || 'Erreur lors de l\'inscription';
const apiErrors = data.errors;
if (apiErrors) {
const mapped: Record<string, string> = {};
Object.entries(apiErrors).forEach(([k, v]) => { mapped[k] = Array.isArray(v) ? v[0] as string : String(v); });
+25 -6
View File
@@ -1,12 +1,31 @@
import axios from 'axios';
import axios, { isAxiosError } from 'axios';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
const API_URL = Platform.select({
android: 'http://10.0.2.2:8000/api/v1',
ios: 'http://localhost:8000/api/v1',
default: 'http://localhost:8000/api/v1',
});
/** Forme standard d'une réponse d'erreur de l'API Laravel. */
export interface ApiErrorData {
message?: string;
errors?: Record<string, string[] | string>;
}
/** Extrait proprement le corps d'erreur d'une exception (sans recourir à `any`). */
export function getApiError(error: unknown): ApiErrorData {
if (isAxiosError(error)) {
return (error.response?.data as ApiErrorData) ?? {};
}
return {};
}
// En production, définir EXPO_PUBLIC_API_URL (ex. https://api.cesizen.fr/api/v1)
// dans l'environnement de build (eas.json / .env). À défaut, on retombe sur la
// configuration de développement local par plateforme (émulateur Android = 10.0.2.2).
const API_URL =
process.env.EXPO_PUBLIC_API_URL ||
Platform.select({
android: 'http://10.0.2.2:8000/api/v1',
ios: 'http://localhost:8000/api/v1',
default: 'http://localhost:8000/api/v1',
});
const api = axios.create({
baseURL: API_URL,