feat: add user fields
This commit is contained in:
parent
9d67a2c335
commit
6ac36e3de4
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum PacePreference: string
|
||||||
|
{
|
||||||
|
case SLOW = 'slow';
|
||||||
|
case NORMAL = 'normal';
|
||||||
|
case FAST = 'fast';
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum UserSex: string
|
||||||
|
{
|
||||||
|
case MAN = 'man';
|
||||||
|
case WOMAN = 'woman';
|
||||||
|
case OTHER = 'other';
|
||||||
|
case UNKNOWN = 'unknown';
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue