feat: add user fields
Laravel CI-CD / Tests Unitaires (push) Successful in 23s Details
Laravel CI-CD / Deploy with Kamal (push) Successful in 1m49s Details

This commit is contained in:
Leon Morival 2026-05-21 12:02:02 +02:00
parent 9d67a2c335
commit 6ac36e3de4
12 changed files with 262 additions and 15 deletions

View File

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum PacePreference: string
{
case SLOW = 'slow';
case NORMAL = 'normal';
case FAST = 'fast';
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
enum PhysicalActivityLevel: string
{
case SEDENTARY = 'sedentary';
case LIGHTLY_ACTIVE = 'lightly_active';
case MODERATELY_ACTIVE = 'moderately_active';
case VERY_ACTIVE = 'very_active';
case ATHLETE = 'athlete';
}

11
app/Enums/UserSex.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum UserSex: string
{
case MAN = 'man';
case WOMAN = 'woman';
case OTHER = 'other';
case UNKNOWN = 'unknown';
}

10
app/Enums/WeightGoal.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum WeightGoal: string
{
case LOSE_WEIGHT = 'lose_weight';
case MAINTAIN_WEIGHT = 'maintain_weight';
case GAIN_WEIGHT = 'gain_weight';
}

View File

@ -12,6 +12,7 @@ use Illuminate\Auth\Events\Verified;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -29,12 +30,12 @@ class AuthController extends Controller
$avatarPath = $request->file('avatar')->store('avatars', 'public'); $avatarPath = $request->file('avatar')->store('avatars', 'public');
} }
$user = User::create([ $userAttributes = $this->userAttributesFromRequestData($data);
'name' => $data['name'], $userAttributes['email'] = strtolower($data['email']);
'email' => strtolower($data['email']), $userAttributes['password'] = Hash::make($data['password']);
'password' => Hash::make($data['password']), $userAttributes['avatar_url'] = $avatarPath;
'avatar_url' => $avatarPath,
]); $user = User::create($userAttributes);
$user->sendEmailVerificationNotification(); $user->sendEmailVerificationNotification();
@ -158,18 +159,17 @@ class AuthController extends Controller
$data['avatar_url'] = $path; $data['avatar_url'] = $path;
} }
unset($data['avatar']); $userAttributes = $this->userAttributesFromRequestData($data);
unset($data['nutritionGoals']);
if (is_array($nutritionGoals)) { if (is_array($nutritionGoals)) {
$data['daily_calorie_goal'] = $nutritionGoals['calories']; $userAttributes['daily_calorie_goal'] = $nutritionGoals['calories'];
$data['daily_protein_goal'] = $nutritionGoals['proteins']; $userAttributes['daily_protein_goal'] = $nutritionGoals['proteins'];
$data['daily_carbs_goal'] = $nutritionGoals['carbs']; $userAttributes['daily_carbs_goal'] = $nutritionGoals['carbs'];
$data['daily_fats_goal'] = $nutritionGoals['fats']; $userAttributes['daily_fats_goal'] = $nutritionGoals['fats'];
} }
if (! empty($data)) { if (! empty($userAttributes)) {
$user->update($data); $user->update($userAttributes);
} }
return new UserResource($user->fresh()); return new UserResource($user->fresh());
@ -212,4 +212,35 @@ class AuthController extends Controller
{ {
return filled($path) && ! Str::startsWith($path, ['http://', 'https://']); return filled($path) && ! Str::startsWith($path, ['http://', 'https://']);
} }
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private function userAttributesFromRequestData(array $data): array
{
$attributes = Arr::only($data, [
'name',
'avatar_url',
'height',
'bio',
'sex',
'date_of_birth',
]);
$attributeMap = [
'physicalActivityLevel' => 'physical_activity_level',
'weightGoal' => 'weight_goal',
'pacePreference' => 'pace_preference',
'dateOfBirth' => 'date_of_birth',
];
foreach ($attributeMap as $requestKey => $attribute) {
if (array_key_exists($requestKey, $data)) {
$attributes[$attribute] = $data[$requestKey];
}
}
return $attributes;
}
} }

View File

@ -2,7 +2,12 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Enums\PacePreference;
use App\Enums\PhysicalActivityLevel;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RegisterRequest extends FormRequest class RegisterRequest extends FormRequest
{ {
@ -28,6 +33,12 @@ class RegisterRequest extends FormRequest
'password' => ['required', 'string', 'min:8'], 'password' => ['required', 'string', 'min:8'],
'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'], 'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'],
'bio' => ['nullable', 'string'], 'bio' => ['nullable', 'string'],
'physicalActivityLevel' => ['sometimes', Rule::enum(PhysicalActivityLevel::class)],
'weightGoal' => ['sometimes', Rule::enum(WeightGoal::class)],
'pacePreference' => ['sometimes', Rule::enum(PacePreference::class)],
'sex' => ['sometimes', Rule::enum(UserSex::class)],
'dateOfBirth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'],
'date_of_birth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'],
]; ];
} }

View File

@ -2,7 +2,12 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Enums\PacePreference;
use App\Enums\PhysicalActivityLevel;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateUserRequest extends FormRequest class UpdateUserRequest extends FormRequest
{ {
@ -23,6 +28,12 @@ class UpdateUserRequest extends FormRequest
'nutritionGoals.proteins' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'], 'nutritionGoals.proteins' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'],
'nutritionGoals.carbs' => ['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'], 'nutritionGoals.fats' => ['required_with:nutritionGoals', 'numeric', 'min:0', 'max:10000'],
'physicalActivityLevel' => ['sometimes', Rule::enum(PhysicalActivityLevel::class)],
'weightGoal' => ['sometimes', Rule::enum(WeightGoal::class)],
'pacePreference' => ['sometimes', Rule::enum(PacePreference::class)],
'sex' => ['sometimes', Rule::enum(UserSex::class)],
'dateOfBirth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'],
'date_of_birth' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'before:today', 'after:1900-01-01'],
]; ];
} }

View File

@ -23,6 +23,11 @@ class UserResource extends JsonResource
'height' => $this->height, 'height' => $this->height,
'avatarUrl' => $this->avatar_url ? asset(Storage::url($this->avatar_url)) : null, 'avatarUrl' => $this->avatar_url ? asset(Storage::url($this->avatar_url)) : null,
'bio' => $this->bio, 'bio' => $this->bio,
'physicalActivityLevel' => $this->physical_activity_level,
'weightGoal' => $this->weight_goal,
'pacePreference' => $this->pace_preference,
'sex' => $this->sex,
'dateOfBirth' => $this->date_of_birth?->toDateString(),
'nutritionGoals' => [ 'nutritionGoals' => [
'calories' => (int) $this->daily_calorie_goal, 'calories' => (int) $this->daily_calorie_goal,
'proteins' => (float) $this->daily_protein_goal, 'proteins' => (float) $this->daily_protein_goal,

View File

@ -2,6 +2,10 @@
namespace App\Models; namespace App\Models;
use App\Enums\PacePreference;
use App\Enums\PhysicalActivityLevel;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\FilamentUser;
use Filament\Panel; use Filament\Panel;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
@ -35,6 +39,11 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
'daily_protein_goal', 'daily_protein_goal',
'daily_carbs_goal', 'daily_carbs_goal',
'daily_fats_goal', 'daily_fats_goal',
'physical_activity_level',
'weight_goal',
'pace_preference',
'sex',
'date_of_birth',
]; ];
/** /**
@ -61,6 +70,11 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
'daily_protein_goal' => 'float', 'daily_protein_goal' => 'float',
'daily_carbs_goal' => 'float', 'daily_carbs_goal' => 'float',
'daily_fats_goal' => 'float', 'daily_fats_goal' => 'float',
'physical_activity_level' => PhysicalActivityLevel::class,
'weight_goal' => WeightGoal::class,
'pace_preference' => PacePreference::class,
'sex' => UserSex::class,
'date_of_birth' => 'immutable_date',
]; ];
} }

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->date('date_of_birth')->nullable();
$table->string('sex')->default('unknown');
$table->string('physical_activity_level')->default('sedentary');
$table->string('weight_goal')->default('lose_weight');
$table->string('pace_preference')->default('slow');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'date_of_birth',
'sex',
'physical_activity_level',
'weight_goal',
'pace_preference',
]);
});
}
};

View File

@ -1,5 +1,9 @@
<?php <?php
use App\Enums\PacePreference;
use App\Enums\PhysicalActivityLevel;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use App\Mail\VerifyAccount; use App\Mail\VerifyAccount;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Notifications\VerifyEmail; use Illuminate\Auth\Notifications\VerifyEmail;
@ -17,15 +21,35 @@ it('sends a verification email when a user registers', function () {
'name' => 'Léon', 'name' => 'Léon',
'email' => 'leon@example.com', 'email' => 'leon@example.com',
'password' => 'password', 'password' => 'password',
'physicalActivityLevel' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value,
'weightGoal' => WeightGoal::MAINTAIN_WEIGHT->value,
'pacePreference' => PacePreference::NORMAL->value,
'sex' => UserSex::WOMAN->value,
'dateOfBirth' => '2003-04-24',
]) ])
->assertCreated() ->assertCreated()
->assertJsonPath('user.email', 'leon@example.com') ->assertJsonPath('user.email', 'leon@example.com')
->assertJsonPath('user.emailVerified', false); ->assertJsonPath('user.emailVerified', false)
->assertJsonPath('user.physicalActivityLevel', PhysicalActivityLevel::LIGHTLY_ACTIVE->value)
->assertJsonPath('user.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value)
->assertJsonPath('user.pacePreference', PacePreference::NORMAL->value)
->assertJsonPath('user.sex', UserSex::WOMAN->value)
->assertJsonPath('user.dateOfBirth', '2003-04-24');
$user = User::where('email', 'leon@example.com')->firstOrFail(); $user = User::where('email', 'leon@example.com')->firstOrFail();
expect($user->hasVerifiedEmail())->toBeFalse(); expect($user->hasVerifiedEmail())->toBeFalse();
$this->assertDatabaseHas('users', [
'id' => $user->id,
'physical_activity_level' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value,
'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value,
'pace_preference' => PacePreference::NORMAL->value,
'sex' => UserSex::WOMAN->value,
]);
expect($user->date_of_birth?->toDateString())->toBe('2003-04-24');
Notification::assertSentTo($user, VerifyEmail::class); Notification::assertSentTo($user, VerifyEmail::class);
}); });

View File

@ -1,5 +1,9 @@
<?php <?php
use App\Enums\PacePreference;
use App\Enums\PhysicalActivityLevel;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum; use Laravel\Sanctum\Sanctum;
@ -71,3 +75,69 @@ it('validates nutrition goals', function () {
'nutritionGoals.fats', 'nutritionGoals.fats',
]); ]);
}); });
it('returns profile preferences with the authenticated user', function () {
$user = User::factory()->create([
'physical_activity_level' => PhysicalActivityLevel::VERY_ACTIVE->value,
'weight_goal' => WeightGoal::GAIN_WEIGHT->value,
'pace_preference' => PacePreference::FAST->value,
]);
Sanctum::actingAs($user);
$this->getJson('/api/auth/me')
->assertOk()
->assertJsonPath('data.physicalActivityLevel', PhysicalActivityLevel::VERY_ACTIVE->value)
->assertJsonPath('data.weightGoal', WeightGoal::GAIN_WEIGHT->value)
->assertJsonPath('data.pacePreference', PacePreference::FAST->value);
});
it('updates profile preferences for the authenticated user', function () {
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->patchJson('/api/auth/me', [
'physicalActivityLevel' => PhysicalActivityLevel::MODERATELY_ACTIVE->value,
'weightGoal' => WeightGoal::MAINTAIN_WEIGHT->value,
'pacePreference' => PacePreference::NORMAL->value,
'sex' => UserSex::MAN->value,
'dateOfBirth' => '2003-04-24',
])
->assertOk()
->assertJsonPath('data.physicalActivityLevel', PhysicalActivityLevel::MODERATELY_ACTIVE->value)
->assertJsonPath('data.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value)
->assertJsonPath('data.pacePreference', PacePreference::NORMAL->value)
->assertJsonPath('data.sex', UserSex::MAN->value)
->assertJsonPath('data.dateOfBirth', '2003-04-24');
$this->assertDatabaseHas('users', [
'id' => $user->id,
'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE->value,
'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value,
'pace_preference' => PacePreference::NORMAL->value,
'sex' => UserSex::MAN->value,
]);
expect($user->fresh()->date_of_birth?->toDateString())->toBe('2003-04-24');
});
it('validates profile preferences', function () {
Sanctum::actingAs(User::factory()->create());
$this->patchJson('/api/auth/me', [
'physicalActivityLevel' => 'daily',
'weightGoal' => 'bulk',
'pacePreference' => 'urgent',
'sex' => 'x',
'dateOfBirth' => 'tomorrow',
])
->assertUnprocessable()
->assertJsonValidationErrors([
'physicalActivityLevel',
'weightGoal',
'pacePreference',
'sex',
'dateOfBirth',
]);
});