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:
+34
-44
@@ -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}"
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
|
||||
@@ -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é',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ForceJsonResponse;
|
||||
use App\Http\Middleware\JwtMiddleware;
|
||||
use App\Http\Middleware\RoleMiddleware;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
@@ -13,12 +16,12 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
)
|
||||
->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 {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<?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\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;
|
||||
|
||||
protected Utilisateur $admin;
|
||||
|
||||
protected Utilisateur $membre;
|
||||
|
||||
protected string $tokenAdmin;
|
||||
|
||||
protected string $tokenMembre;
|
||||
|
||||
protected function setUp(): void
|
||||
|
||||
@@ -16,7 +16,9 @@ class TrackerTest extends TestCase
|
||||
use RefreshDatabase;
|
||||
|
||||
protected Utilisateur $utilisateur;
|
||||
|
||||
protected Emotion $emotion;
|
||||
|
||||
protected string $token;
|
||||
|
||||
protected function setUp(): void
|
||||
|
||||
@@ -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
|
||||
@@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -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=
|
||||
@@ -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}`)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user