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
+34 -44
View File
@@ -1,65 +1,55 @@
APP_NAME=Laravel APP_NAME=CESIZen
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=false APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost:8000
APP_LOCALE=en # Passer APP_ENV=production et APP_DEBUG=false en production.
APP_FALLBACK_LOCALE=en # Générer les clés au déploiement : `php artisan key:generate` puis `php artisan jwt:secret`.
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file APP_LOCALE=fr
# APP_MAINTENANCE_STORE=database APP_FALLBACK_LOCALE=fr
APP_FAKER_LOCALE=fr_FR
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_STACK=single LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite # Base de données — PostgreSQL 16
# DB_HOST=127.0.0.1 DB_CONNECTION=pgsql
# DB_PORT=3306 DB_HOST=postgres
# DB_DATABASE=laravel DB_PORT=5432
# DB_USERNAME=root DB_DATABASE=cesizen
# DB_PASSWORD= DB_USERNAME=cesizen
DB_PASSWORD=
SESSION_DRIVER=database # Authentification JWT (tymon/jwt-auth)
JWT_SECRET=
JWT_TTL=60
JWT_REFRESH_TTL=20160
# CORS — origine autorisée du frontend web
FRONTEND_URL=http://localhost:3000
# Cache / sessions / files d'attente — Redis en production
CACHE_STORE=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false QUEUE_CONNECTION=redis
SESSION_PATH=/ REDIS_HOST=redis
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
BCRYPT_ROUNDS=12
FILESYSTEM_DISK=local
BROADCAST_CONNECTION=log
MAIL_MAILER=log MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1 MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="no-reply@cesizen.fr"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
@@ -3,9 +3,8 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Utilisateur;
use App\Models\Role;
use App\Models\Tracker; use App\Models\Tracker;
use App\Models\Utilisateur;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -2,11 +2,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\RegisterRequest;
use App\Http\Requests\LoginRequest; use App\Http\Requests\LoginRequest;
use App\Models\Utilisateur; use App\Http\Requests\RegisterRequest;
use App\Models\Role; use App\Models\Role;
use App\Models\Tracker; use App\Models\Tracker;
use App\Models\Utilisateur;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Facades\JWTAuth;
@@ -54,6 +54,7 @@ class AuthController extends Controller
if (! $utilisateur->est_actif) { if (! $utilisateur->est_actif) {
auth()->logout(); auth()->logout();
return response()->json([ return response()->json([
'message' => 'Votre compte a été désactivé', 'message' => 'Votre compte a été désactivé',
], 403); ], 403);
@@ -3,7 +3,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\StoreSaisieRequest; use App\Http\Requests\StoreSaisieRequest;
use App\Models\SaisieTracker;
use App\Services\RapportService; use App\Services\RapportService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -4,10 +4,10 @@ namespace App\Http\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException; use Tymon\JWTAuth\Exceptions\TokenInvalidException;
use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Facades\JWTAuth;
class JwtMiddleware class JwtMiddleware
{ {
-2
View File
@@ -3,9 +3,7 @@
namespace App\Services; namespace App\Services;
use App\Models\Utilisateur; use App\Models\Utilisateur;
use App\Models\SaisieTracker;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class RapportService class RapportService
{ {
+6 -3
View File
@@ -1,5 +1,8 @@
<?php <?php
use App\Http\Middleware\ForceJsonResponse;
use App\Http\Middleware\JwtMiddleware;
use App\Http\Middleware\RoleMiddleware;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
@@ -13,12 +16,12 @@ return Application::configure(basePath: dirname(__DIR__))
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([ $middleware->alias([
'jwt.auth' => \App\Http\Middleware\JwtMiddleware::class, 'jwt.auth' => JwtMiddleware::class,
'role' => \App\Http\Middleware\RoleMiddleware::class, 'role' => RoleMiddleware::class,
]); ]);
$middleware->api(prepend: [ $middleware->api(prepend: [
\App\Http\Middleware\ForceJsonResponse::class, ForceJsonResponse::class,
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
+8 -8
View File
@@ -1,14 +1,14 @@
<?php <?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\ProfilController;
use App\Http\Controllers\FeedController;
use App\Http\Controllers\EmotionController;
use App\Http\Controllers\SaisieTrackerController;
use App\Http\Controllers\Admin\AdminUtilisateurController;
use App\Http\Controllers\Admin\AdminFeedController;
use App\Http\Controllers\Admin\AdminEmotionController; use App\Http\Controllers\Admin\AdminEmotionController;
use App\Http\Controllers\Admin\AdminFeedController;
use App\Http\Controllers\Admin\AdminUtilisateurController;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\EmotionController;
use App\Http\Controllers\FeedController;
use App\Http\Controllers\ProfilController;
use App\Http\Controllers\SaisieTrackerController;
use Illuminate\Support\Facades\Route;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -14,8 +14,11 @@ class AdminUtilisateurTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
protected Utilisateur $admin; protected Utilisateur $admin;
protected Utilisateur $membre; protected Utilisateur $membre;
protected string $tokenAdmin; protected string $tokenAdmin;
protected string $tokenMembre; protected string $tokenMembre;
protected function setUp(): void protected function setUp(): void
+2
View File
@@ -16,7 +16,9 @@ class TrackerTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
protected Utilisateur $utilisateur; protected Utilisateur $utilisateur;
protected Emotion $emotion; protected Emotion $emotion;
protected string $token; protected string $token;
protected function setUp(): void protected function setUp(): void
+3
View File
@@ -0,0 +1,3 @@
# URL publique de l'API REST CESIZen.
# En production, pointer vers l'API HTTPS (ex. https://api.cesizen.fr/api/v1).
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
+1
View File
@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env*
!.env.example
# vercel # vercel
.vercel .vercel
+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 { useRouter } from 'expo-router';
import { useFeeds } from '@/hooks/useFeeds'; import { useFeeds } from '@/hooks/useFeeds';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import type { Feed } from '@/types';
export default function InfoListScreen() { export default function InfoListScreen() {
const { data: feeds, isLoading } = useFeeds(); const { data: feeds, isLoading } = useFeeds();
const router = useRouter(); const router = useRouter();
const renderItem = ({ item }: { item: any }) => ( const renderItem = ({ item }: { item: Feed }) => (
<TouchableOpacity <TouchableOpacity
style={styles.card} style={styles.card}
onPress={() => router.push(`/(tabs)/info/${item.slug}`)} 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 ( return (
<TouchableOpacity style={styles.menuItem}> <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> <Text style={styles.menuLabel}>{label}</Text>
<Ionicons name="chevron-forward" size={18} color={Colors.gray[300]} /> <Ionicons name="chevron-forward" size={18} color={Colors.gray[300]} />
</TouchableOpacity> </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 { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Link, useRouter } from 'expo-router'; import { Link, useRouter } from 'expo-router';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
export default function LoginScreen() { export default function LoginScreen() {
@@ -20,8 +21,8 @@ export default function LoginScreen() {
try { try {
await login(email, password); await login(email, password);
router.replace('/(tabs)'); router.replace('/(tabs)');
} catch (error: any) { } catch (error) {
Alert.alert('Erreur', error.response?.data?.message || 'Identifiants incorrects'); Alert.alert('Erreur', getApiError(error).message || 'Identifiants incorrects');
} finally { } finally {
setLoading(false); 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 { Link, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -43,9 +44,10 @@ export default function RegisterScreen() {
consentement_rgpd: consentement, consentement_rgpd: consentement,
}); });
router.replace('/(tabs)'); router.replace('/(tabs)');
} catch (error: any) { } catch (error) {
const msg = error.response?.data?.message || 'Erreur lors de l\'inscription'; const data = getApiError(error);
const apiErrors = error.response?.data?.errors; const msg = data.message || 'Erreur lors de l\'inscription';
const apiErrors = data.errors;
if (apiErrors) { if (apiErrors) {
const mapped: Record<string, string> = {}; const mapped: Record<string, string> = {};
Object.entries(apiErrors).forEach(([k, v]) => { mapped[k] = Array.isArray(v) ? v[0] as string : String(v); }); Object.entries(apiErrors).forEach(([k, v]) => { mapped[k] = Array.isArray(v) ? v[0] as string : String(v); });
+21 -2
View File
@@ -1,8 +1,27 @@
import axios from 'axios'; import axios, { isAxiosError } from 'axios';
import * as SecureStore from 'expo-secure-store'; import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
const API_URL = Platform.select({ /** 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', android: 'http://10.0.2.2:8000/api/v1',
ios: 'http://localhost:8000/api/v1', ios: 'http://localhost:8000/api/v1',
default: 'http://localhost:8000/api/v1', default: 'http://localhost:8000/api/v1',