diff --git a/backend/.env.example b/backend/.env.example index 71447c3..adabe9e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,65 +1,55 @@ -APP_NAME=Laravel +APP_NAME=CESIZen APP_ENV=local APP_KEY= -APP_DEBUG=false -APP_URL=http://localhost +APP_DEBUG=true +APP_URL=http://localhost:8000 -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US +# Passer APP_ENV=production et APP_DEBUG=false en production. +# Générer les clés au déploiement : `php artisan key:generate` puis `php artisan jwt:secret`. -APP_MAINTENANCE_DRIVER=file -# APP_MAINTENANCE_STORE=database - -# PHP_CLI_SERVER_WORKERS=4 - -BCRYPT_ROUNDS=12 +APP_LOCALE=fr +APP_FALLBACK_LOCALE=fr +APP_FAKER_LOCALE=fr_FR LOG_CHANNEL=stack LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +# Base de données — PostgreSQL 16 +DB_CONNECTION=pgsql +DB_HOST=postgres +DB_PORT=5432 +DB_DATABASE=cesizen +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_ENCRYPT=false -SESSION_PATH=/ -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 +QUEUE_CONNECTION=redis +REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 +BCRYPT_ROUNDS=12 + +FILESYSTEM_DISK=local +BROADCAST_CONNECTION=log + MAIL_MAILER=log -MAIL_SCHEME=null MAIL_HOST=127.0.0.1 MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null -MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_ADDRESS="no-reply@cesizen.fr" 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}" diff --git a/backend/app/Http/Controllers/Admin/AdminEmotionController.php b/backend/app/Http/Controllers/Admin/AdminEmotionController.php index e543b9f..f7ada50 100644 --- a/backend/app/Http/Controllers/Admin/AdminEmotionController.php +++ b/backend/app/Http/Controllers/Admin/AdminEmotionController.php @@ -12,7 +12,7 @@ class AdminEmotionController extends Controller public function index(): JsonResponse { $emotions = Emotion::niveau(1) - ->with(['enfants' => fn($q) => $q->orderBy('nom')]) + ->with(['enfants' => fn ($q) => $q->orderBy('nom')]) ->orderBy('nom') ->get(); diff --git a/backend/app/Http/Controllers/Admin/AdminFeedController.php b/backend/app/Http/Controllers/Admin/AdminFeedController.php index dbfdde1..7efa1d5 100644 --- a/backend/app/Http/Controllers/Admin/AdminFeedController.php +++ b/backend/app/Http/Controllers/Admin/AdminFeedController.php @@ -39,7 +39,7 @@ class AdminFeedController extends Controller $feed = Feed::findOrFail($id); $data = $request->validated(); - if (isset($data['titre']) && !isset($data['slug'])) { + if (isset($data['titre']) && ! isset($data['slug'])) { $data['slug'] = Str::slug($data['titre']); } diff --git a/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php b/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php index 0df76db..0f4c857 100644 --- a/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php +++ b/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php @@ -3,9 +3,8 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; -use App\Models\Utilisateur; -use App\Models\Role; use App\Models\Tracker; +use App\Models\Utilisateur; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -19,13 +18,13 @@ class AdminUtilisateurController extends Controller $search = $request->search; $query->where(function ($q) use ($search) { $q->where('nom', 'like', "%{$search}%") - ->orWhere('prenom', 'like', "%{$search}%") - ->orWhere('email', 'like', "%{$search}%"); + ->orWhere('prenom', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); }); } if ($request->filled('role')) { - $query->whereHas('role', fn($q) => $q->where('nom', $request->role)); + $query->whereHas('role', fn ($q) => $q->where('nom', $request->role)); } $utilisateurs = $query->orderBy('created_at', 'desc')->paginate(15); @@ -88,7 +87,7 @@ class AdminUtilisateurController extends Controller public function toggleActive(int $id): JsonResponse { $utilisateur = Utilisateur::findOrFail($id); - $utilisateur->update(['est_actif' => !$utilisateur->est_actif]); + $utilisateur->update(['est_actif' => ! $utilisateur->est_actif]); return response()->json([ 'message' => $utilisateur->est_actif ? 'Compte activé' : 'Compte désactivé', diff --git a/backend/app/Http/Controllers/AuthController.php b/backend/app/Http/Controllers/AuthController.php index 82acb22..dda877d 100644 --- a/backend/app/Http/Controllers/AuthController.php +++ b/backend/app/Http/Controllers/AuthController.php @@ -2,11 +2,11 @@ namespace App\Http\Controllers; -use App\Http\Requests\RegisterRequest; use App\Http\Requests\LoginRequest; -use App\Models\Utilisateur; +use App\Http\Requests\RegisterRequest; use App\Models\Role; use App\Models\Tracker; +use App\Models\Utilisateur; use Illuminate\Http\JsonResponse; use Tymon\JWTAuth\Facades\JWTAuth; @@ -44,7 +44,7 @@ class AuthController extends Controller { $credentials = $request->only('email', 'password'); - if (!$token = auth()->attempt($credentials)) { + if (! $token = auth()->attempt($credentials)) { return response()->json([ 'message' => 'Identifiants incorrects', ], 401); @@ -52,8 +52,9 @@ class AuthController extends Controller $utilisateur = auth()->user(); - if (!$utilisateur->est_actif) { + if (! $utilisateur->est_actif) { auth()->logout(); + return response()->json([ 'message' => 'Votre compte a été désactivé', ], 403); diff --git a/backend/app/Http/Controllers/ProfilController.php b/backend/app/Http/Controllers/ProfilController.php index 469b879..c88419d 100644 --- a/backend/app/Http/Controllers/ProfilController.php +++ b/backend/app/Http/Controllers/ProfilController.php @@ -35,7 +35,7 @@ class ProfilController extends Controller { $utilisateur = auth()->user(); - if (!Hash::check($request->ancien_mot_de_passe, $utilisateur->password)) { + if (! Hash::check($request->ancien_mot_de_passe, $utilisateur->password)) { return response()->json([ 'message' => 'L\'ancien mot de passe est incorrect', ], 422); diff --git a/backend/app/Http/Controllers/SaisieTrackerController.php b/backend/app/Http/Controllers/SaisieTrackerController.php index 1649042..c8b26ee 100644 --- a/backend/app/Http/Controllers/SaisieTrackerController.php +++ b/backend/app/Http/Controllers/SaisieTrackerController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers; use App\Http\Requests\StoreSaisieRequest; -use App\Models\SaisieTracker; use App\Services\RapportService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -14,7 +13,7 @@ class SaisieTrackerController extends Controller { $tracker = auth()->user()->tracker; - if (!$tracker) { + if (! $tracker) { return response()->json([]); } @@ -39,7 +38,7 @@ class SaisieTrackerController extends Controller { $tracker = auth()->user()->tracker; - if (!$tracker) { + if (! $tracker) { $tracker = auth()->user()->tracker()->create(['nom' => 'Mon journal']); } diff --git a/backend/app/Http/Middleware/JwtMiddleware.php b/backend/app/Http/Middleware/JwtMiddleware.php index ee53fea..55cf226 100644 --- a/backend/app/Http/Middleware/JwtMiddleware.php +++ b/backend/app/Http/Middleware/JwtMiddleware.php @@ -4,10 +4,10 @@ namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; -use Tymon\JWTAuth\Facades\JWTAuth; +use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; -use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Facades\JWTAuth; class JwtMiddleware { diff --git a/backend/app/Http/Middleware/RoleMiddleware.php b/backend/app/Http/Middleware/RoleMiddleware.php index ce6f617..6f7736d 100644 --- a/backend/app/Http/Middleware/RoleMiddleware.php +++ b/backend/app/Http/Middleware/RoleMiddleware.php @@ -11,7 +11,7 @@ class RoleMiddleware { $user = auth()->user(); - if (!$user || !$user->role || !in_array($user->role->nom, $roles)) { + if (! $user || ! $user->role || ! in_array($user->role->nom, $roles)) { return response()->json(['message' => 'Accès non autorisé'], 403); } diff --git a/backend/app/Services/RapportService.php b/backend/app/Services/RapportService.php index 1b4e6ed..85bbe31 100644 --- a/backend/app/Services/RapportService.php +++ b/backend/app/Services/RapportService.php @@ -3,9 +3,7 @@ namespace App\Services; use App\Models\Utilisateur; -use App\Models\SaisieTracker; use Carbon\Carbon; -use Illuminate\Support\Facades\DB; class RapportService { @@ -13,7 +11,7 @@ class RapportService { $tracker = $user->tracker; - if (!$tracker) { + if (! $tracker) { return $this->rapportVide(); } @@ -35,7 +33,7 @@ class RapportService $intensiteMoyenne = round($saisies->avg('intensite'), 1); $emotionsDominantes = $saisies->groupBy('emotion_id') - ->map(fn($group) => [ + ->map(fn ($group) => [ 'emotion' => $group->first()->emotion, 'count' => $group->count(), 'intensite_moyenne' => round($group->avg('intensite'), 1), @@ -47,7 +45,7 @@ class RapportService $evolution = $this->getEvolution($saisies, $period, $dateDebut, $dateFin); - $repartition = $emotionsDominantes->map(fn($item) => [ + $repartition = $emotionsDominantes->map(fn ($item) => [ 'nom' => $item['emotion']->nom, 'couleur' => $item['emotion']->couleur, 'icone' => $item['emotion']->icone, @@ -96,7 +94,7 @@ class RapportService default => 'Y-m-d', }; - $grouped = $saisies->groupBy(fn($saisie) => $saisie->date_saisie->format($format)); + $grouped = $saisies->groupBy(fn ($saisie) => $saisie->date_saisie->format($format)); $labels = []; $dataIntensiteMoyenne = []; @@ -108,7 +106,7 @@ class RapportService $emotionDatasets = $saisies->groupBy('emotion_id')->map(function ($emotionGroup) use ($format, $grouped) { $emotion = $emotionGroup->first()->emotion; - $byPeriod = $emotionGroup->groupBy(fn($s) => $s->date_saisie->format($format)); + $byPeriod = $emotionGroup->groupBy(fn ($s) => $s->date_saisie->format($format)); $data = []; foreach ($grouped->keys() as $label) { diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index 515bc43..4e2507e 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -1,5 +1,8 @@ withMiddleware(function (Middleware $middleware): void { $middleware->alias([ - 'jwt.auth' => \App\Http\Middleware\JwtMiddleware::class, - 'role' => \App\Http\Middleware\RoleMiddleware::class, + 'jwt.auth' => JwtMiddleware::class, + 'role' => RoleMiddleware::class, ]); $middleware->api(prepend: [ - \App\Http\Middleware\ForceJsonResponse::class, + ForceJsonResponse::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/backend/routes/api.php b/backend/routes/api.php index d1d3a31..0da937f 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,14 +1,14 @@ ( + const renderItem = ({ item }: { item: Feed }) => ( router.push(`/(tabs)/info/${item.slug}`)} diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx index b704ed2..c13bbc2 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -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 ( - + {label} diff --git a/mobile/app/auth/login.tsx b/mobile/app/auth/login.tsx index 0659d10..00f3ce6 100644 --- a/mobile/app/auth/login.tsx +++ b/mobile/app/auth/login.tsx @@ -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); } diff --git a/mobile/app/auth/register.tsx b/mobile/app/auth/register.tsx index 3c48890..87a19e7 100644 --- a/mobile/app/auth/register.tsx +++ b/mobile/app/auth/register.tsx @@ -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 = {}; Object.entries(apiErrors).forEach(([k, v]) => { mapped[k] = Array.isArray(v) ? v[0] as string : String(v); }); diff --git a/mobile/lib/api.ts b/mobile/lib/api.ts index 85927d2..9bc9156 100644 --- a/mobile/lib/api.ts +++ b/mobile/lib/api.ts @@ -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; +} + +/** 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,