From 6b757c37be90ccb622ed84d3bb4487400ed33982 Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Fri, 7 Aug 2026 16:10:25 +0200 Subject: [PATCH] feat: accept terms --- app/Http/Controllers/AuthController.php | 1 + app/Http/Requests/RegisterRequest.php | 1 + app/Models/User.php | 2 ++ ...0_add_terms_accepted_at_to_users_table.php | 28 +++++++++++++++++ tests/Feature/EmailVerificationTest.php | 31 ++++++++++++++++++- 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_08_07_135640_add_terms_accepted_at_to_users_table.php diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index dc64b96..d3631e3 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -360,6 +360,7 @@ class AuthController extends Controller $userAttributes['email'] = strtolower($data['email']); $userAttributes['password'] = Hash::make($data['password']); $userAttributes['avatar_url'] = $avatarPath; + $userAttributes['terms_accepted_at'] = now(); $trialDays = max(0, (int) config('billing.trial_days', 7)); if ($trialDays > 0) { diff --git a/app/Http/Requests/RegisterRequest.php b/app/Http/Requests/RegisterRequest.php index 02d0e99..a11ba20 100644 --- a/app/Http/Requests/RegisterRequest.php +++ b/app/Http/Requests/RegisterRequest.php @@ -50,6 +50,7 @@ class RegisterRequest extends FormRequest ->symbols() ->uncompromised(), ], + 'termsAccepted' => ['required', 'accepted'], 'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'], 'bio' => ['nullable', 'string'], 'physicalActivityLevel' => ['sometimes', Rule::enum(PhysicalActivityLevel::class)], diff --git a/app/Models/User.php b/app/Models/User.php index 196305b..1f81c89 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -58,6 +58,7 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale 'pace_preference', 'sex', 'date_of_birth', + 'terms_accepted_at', 'suspended_at', 'suspended_by', 'suspended_reason', @@ -95,6 +96,7 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale 'sex' => UserSex::class, 'role' => UserRole::class, 'date_of_birth' => 'immutable_date', + 'terms_accepted_at' => 'datetime', 'suspended_at' => 'datetime', 'trial_ends_at' => 'datetime', ]; diff --git a/database/migrations/2026_08_07_135640_add_terms_accepted_at_to_users_table.php b/database/migrations/2026_08_07_135640_add_terms_accepted_at_to_users_table.php new file mode 100644 index 0000000..c2b85df --- /dev/null +++ b/database/migrations/2026_08_07_135640_add_terms_accepted_at_to_users_table.php @@ -0,0 +1,28 @@ +timestampTz('terms_accepted_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('terms_accepted_at'); + }); + } +}; diff --git a/tests/Feature/EmailVerificationTest.php b/tests/Feature/EmailVerificationTest.php index 6675fd7..605f0a3 100644 --- a/tests/Feature/EmailVerificationTest.php +++ b/tests/Feature/EmailVerificationTest.php @@ -27,6 +27,7 @@ it('sends a verification email when a user registers', function () { 'pacePreference' => PacePreference::NORMAL->value, 'sex' => UserSex::WOMAN->value, 'dateOfBirth' => '2003-04-24', + 'termsAccepted' => true, ]) ->assertCreated() ->assertJsonPath('user.email', 'leon@example.com') @@ -45,7 +46,8 @@ it('sends a verification email when a user registers', function () { expect($user->hasVerifiedEmail())->toBeFalse() ->and($user->trial_ends_at)->not->toBeNull() - ->and($user->trial_ends_at->isFuture())->toBeTrue(); + ->and($user->trial_ends_at->isFuture())->toBeTrue() + ->and($user->terms_accepted_at)->not->toBeNull(); $this->assertDatabaseHas('users', [ 'id' => $user->id, @@ -71,6 +73,7 @@ it('returns a temporary error when the verification email cannot be sent during 'email' => 'leon@example.com', 'password' => 'Motsdfdepasse123*', 'locale' => 'fr-FR', + 'termsAccepted' => true, ]) ->assertServiceUnavailable() ->assertJsonPath('message', __('api.auth.verification_email_failed')); @@ -94,6 +97,7 @@ it('registers mobile users with a bearer token', function () { 'pacePreference' => PacePreference::NORMAL->value, 'sex' => UserSex::MAN->value, 'dateOfBirth' => '2003-04-24', + 'termsAccepted' => true, ]) ->assertCreated() ->assertJsonPath('user.email', 'mobile.leon@example.com') @@ -111,6 +115,31 @@ it('registers mobile users with a bearer token', function () { Notification::assertSentTo($user, VerifyEmail::class); }); +it('requires users to accept the terms during registration', function () { + Notification::fake(); + + $registrationData = [ + 'name' => 'TermsLeon', + 'email' => 'terms.leon@example.com', + 'password' => 'Motsdfdepasse123*', + 'termsAccepted' => false, + ]; + + $this->postJson('/api/auth/mobile/register', $registrationData) + ->assertUnprocessable() + ->assertJsonValidationErrors('termsAccepted'); + + unset($registrationData['termsAccepted']); + + $this->postJson('/api/auth/mobile/register', $registrationData) + ->assertUnprocessable() + ->assertJsonValidationErrors('termsAccepted'); + + $this->assertDatabaseMissing('users', [ + 'email' => 'terms.leon@example.com', + ]); +}); + it('uses the custom account verification mailable', function () { $user = User::factory()->unverified()->create([ 'locale' => 'fr',