feat: ations

This commit is contained in:
Leon Morival 2026-05-17 13:28:02 +02:00
parent 1964b60949
commit 2b6a680d22
9 changed files with 302 additions and 18 deletions

View File

@ -80,6 +80,7 @@ class AuthController extends Controller
{
$user = $request->user();
$data = $request->validated();
$nutritionGoals = $data['nutritionGoals'] ?? null;
if ($request->hasFile('avatar')) {
if ($user->avatar_url) {
@ -91,6 +92,14 @@ class AuthController extends Controller
}
unset($data['avatar']);
unset($data['nutritionGoals']);
if (is_array($nutritionGoals)) {
$data['daily_calorie_goal'] = $nutritionGoals['calories'];
$data['daily_protein_goal'] = $nutritionGoals['proteins'];
$data['daily_carbs_goal'] = $nutritionGoals['carbs'];
$data['daily_fats_goal'] = $nutritionGoals['fats'];
}
if (! empty($data)) {
$user->update($data);

View File

@ -15,6 +15,11 @@ class UpdateUserRequest extends FormRequest
{
return [
'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'],
'nutritionGoals' => ['sometimes', 'array'],
'nutritionGoals.calories' => ['required_with:nutritionGoals', 'integer', 'min:0', 'max:100000'],
'nutritionGoals.proteins' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'],
'nutritionGoals.carbs' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'],
'nutritionGoals.fats' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'],
];
}
@ -22,6 +27,10 @@ class UpdateUserRequest extends FormRequest
{
return [
'avatar.max' => "L'image ne doit pas dépasser 2 Mo.",
'nutritionGoals.calories.integer' => 'Les calories doivent etre un nombre entier.',
'nutritionGoals.*.required_with' => 'Renseigne tous les objectifs nutritionnels.',
'nutritionGoals.*.min' => 'Les objectifs nutritionnels doivent etre positifs.',
'nutritionGoals.*.max' => 'La valeur de cet objectif nutritionnel est trop elevee.',
];
}
}

View File

@ -20,6 +20,12 @@ class UserResource extends JsonResource
'email' => $this->email,
'avatarUrl' => $this->avatar_url ? asset(Storage::url($this->avatar_url)) : null,
'bio' => $this->bio,
'nutritionGoals' => [
'calories' => (int) $this->daily_calorie_goal,
'proteins' => (float) $this->daily_protein_goal,
'carbs' => (float) $this->daily_carbs_goal,
'fats' => (float) $this->daily_fats_goal,
],
];
}
}

View File

@ -28,6 +28,10 @@ class User extends Authenticatable implements FilamentUser
'password',
'avatar_url',
'bio',
'daily_calorie_goal',
'daily_protein_goal',
'daily_carbs_goal',
'daily_fats_goal',
];
/**
@ -50,6 +54,10 @@ class User extends Authenticatable implements FilamentUser
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'daily_calorie_goal' => 'integer',
'daily_protein_goal' => 'float',
'daily_carbs_goal' => 'float',
'daily_fats_goal' => 'float',
];
}

View File

@ -29,6 +29,10 @@ class UserFactory extends Factory
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'avatar_url' => null,
'daily_calorie_goal' => 2000,
'daily_protein_goal' => 120,
'daily_carbs_goal' => 250,
'daily_fats_goal' => 70,
'remember_token' => Str::random(10),
];
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->unsignedInteger('daily_calorie_goal')->default(2000)->after('avatar_url');
$table->decimal('daily_protein_goal', 8, 2)->default(120)->after('daily_calorie_goal');
$table->decimal('daily_carbs_goal', 8, 2)->default(250)->after('daily_protein_goal');
$table->decimal('daily_fats_goal', 8, 2)->default(70)->after('daily_carbs_goal');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropColumn([
'daily_calorie_goal',
'daily_protein_goal',
'daily_carbs_goal',
'daily_fats_goal',
]);
});
}
};

142
deploy.yml Normal file
View File

@ -0,0 +1,142 @@
services:
app:
image: gitea.leonmorival.com/leon-morival/bemeal-api:latest
container_name: bemeal-app
restart: unless-stopped
depends_on:
pgsql:
condition: service_healthy
redis:
condition: service_started
environment:
APP_NAME: "BeMeal"
APP_ENV: production
APP_DEBUG: "false"
APP_KEY: ${APP_KEY}
APP_URL: ${APP_URL}
APP_LOCALE: fr
SERVER_NAME: ":80"
APP_RUNTIME: "Laravel\\FrankenPHP\\Runtime"
DB_CONNECTION: pgsql
DB_HOST: pgsql
DB_PORT: 5432
DB_DATABASE: ${DB_DATABASE}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_CLIENT: predis
QUEUE_CONNECTION: redis
SCOUT_DRIVER: meilisearch
MEILISEARCH_HOST: http://meilisearch:7700
MEILISEARCH_KEY: ${MEILISEARCH_KEY}
volumes:
- bemeal_storage:/var/www/html/storage/app
- bemeal_public:/var/www/html/storage/app/public
networks:
- nginx
- internal
horizon:
image: gitea.leonmorival.com/leon-morival/bemeal-api:latest
container_name: bemeal-horizon
restart: unless-stopped
command: php artisan horizon
depends_on:
pgsql:
condition: service_healthy
redis:
condition: service_started
environment:
APP_ENV: production
APP_KEY: ${APP_KEY}
DB_CONNECTION: pgsql
DB_HOST: pgsql
DB_PORT: 5432
DB_DATABASE: ${DB_DATABASE}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_CLIENT: predis
QUEUE_CONNECTION: redis
volumes:
- bemeal_storage:/var/www/html/storage/app
networks:
- internal
pgsql:
image: postgres:15-alpine
container_name: bemeal-pgsql
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE}"]
interval: 5s
timeout: 5s
retries: 10
volumes:
- bemeal_pgsql:/var/lib/postgresql/data
networks:
- internal
redis:
image: redis:alpine
container_name: bemeal-redis
restart: unless-stopped
volumes:
- bemeal_redis:/data
networks:
- internal
meilisearch:
image: getmeili/meilisearch:latest
container_name: bemeal-meilisearch
restart: unless-stopped
environment:
MEILI_NO_ANALYTICS: "true"
MEILI_MASTER_KEY: ${MEILISEARCH_KEY}
volumes:
- bemeal_meili:/meili_data
networks:
- internal
volumes:
bemeal_pgsql:
bemeal_redis:
bemeal_storage:
bemeal_public:
bemeal_meili:
networks:
nginx:
external: true
internal:
driver: bridge

View File

@ -1,7 +1,7 @@
services:
starter-api:
image: gitea.leonmorival.com/leon-morival/laravel-starter:latest
container_name: starter-starter-api-1
image: gitea.leonmorival.com/leon-morival/bemeal-api:latest
container_name: bemeal-api
restart: unless-stopped
depends_on:
pgsql:
@ -37,12 +37,12 @@ services:
- storage-data:/var/www/html/storage/app
- storage-public-data:/var/www/html/storage/app/public
networks:
- nginx
- nginx-bemeal
- internal
pgsql:
image: postgres:15-alpine
container_name: starter-pgsql-1
container_name: bemeal-pgsql
restart: unless-stopped
environment:
POSTGRES_DB: "${DB_DATABASE}"
@ -60,7 +60,7 @@ services:
redis:
image: redis:alpine
container_name: starter-redis-1
container_name: bemeal-redis
restart: unless-stopped
volumes:
- redis-data:/data
@ -68,11 +68,14 @@ services:
- internal
horizon:
image: gitea.leonmorival.com/leon-morival/laravel-starter:latest
container_name: starter-horizon-1
image: gitea.leonmorival.com/leon-morival/bemeal-api:latest
container_name: bemeal-horizon
restart: unless-stopped
depends_on:
- redis
pgsql:
condition: service_healthy
redis:
condition: service_started
environment:
APP_ENV: "production"
APP_KEY: "${APP_KEY}"
@ -93,18 +96,17 @@ services:
adminer:
image: adminer:latest
container_name: starter-adminer-1
container_name: bemeal-adminer
restart: unless-stopped
environment:
# Optionnel : définit PostgreSQL par défaut à la connexion
ADMINER_DEFAULT_SERVER: pgsql
networks:
- internal
- nginx
# - nginx
meilisearch:
image: getmeili/meilisearch:latest
container_name: starter-meilisearch-1
container_name: bemeal-meilisearch
restart: unless-stopped
environment:
MEILI_NO_ANALYTICS: "true"
@ -113,22 +115,23 @@ services:
- meilisearch-data:/meili_data
networks:
- internal
- nginx
# - nginx
volumes:
pgsql-data:
external: true
name: starter_pgsql_data
name: bemeal_pgsql_data
redis-data:
name: starter_redis_data
name: bemeal_redis_data
storage-data:
name: bemeal_storage_data
storage-public-data:
name: starter_storage_public_data
name: bemeal_storage_public_data
meilisearch-data:
name: starter_meilisearch_data
name: bemeal_meilisearch_data
networks:
nginx:
nginx-bemeal:
external: true
internal:
driver: bridge

View File

@ -0,0 +1,73 @@
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
uses(RefreshDatabase::class);
it('returns nutrition goals with the authenticated user', function () {
$user = User::factory()->create([
'daily_calorie_goal' => 2400,
'daily_protein_goal' => 140,
'daily_carbs_goal' => 280,
'daily_fats_goal' => 80,
]);
Sanctum::actingAs($user);
$this->getJson('/api/auth/me')
->assertOk()
->assertJsonPath('data.nutritionGoals.calories', 2400)
->assertJsonPath('data.nutritionGoals.proteins', 140)
->assertJsonPath('data.nutritionGoals.carbs', 280)
->assertJsonPath('data.nutritionGoals.fats', 80);
});
it('updates nutrition goals for the authenticated user', function () {
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->patchJson('/api/auth/me', [
'nutritionGoals' => [
'calories' => 2200,
'proteins' => 135.5,
'carbs' => 260,
'fats' => 75.25,
],
]);
$response
->assertOk()
->assertJsonPath('data.nutritionGoals.calories', 2200)
->assertJsonPath('data.nutritionGoals.proteins', 135.5)
->assertJsonPath('data.nutritionGoals.carbs', 260)
->assertJsonPath('data.nutritionGoals.fats', 75.25);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'daily_calorie_goal' => 2200,
'daily_protein_goal' => 135.5,
'daily_carbs_goal' => 260,
'daily_fats_goal' => 75.25,
]);
});
it('validates nutrition goals', function () {
Sanctum::actingAs(User::factory()->create());
$this->patchJson('/api/auth/me', [
'nutritionGoals' => [
'calories' => -1,
'proteins' => 'abc',
],
])
->assertUnprocessable()
->assertJsonValidationErrors([
'nutritionGoals.calories',
'nutritionGoals.proteins',
'nutritionGoals.carbs',
'nutritionGoals.fats',
]);
});