diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -151,10 +151,11 @@ * Validation of the verification code. * * @param \Illuminate\Http\Request $request HTTP request + * @param bool $update Update the signup code record * * @return \Illuminate\Http\JsonResponse JSON response */ - public function verify(Request $request) + public function verify(Request $request, $update = true) { // Validate the request args $v = Validator::make( @@ -182,9 +183,14 @@ } // For signup last-step mode remember the code object, so we can delete it - // with single SQL query (->delete()) instead of two (::destroy()) + // with single SQL query (->delete()) instead of two $request->code = $code; + if ($update) { + $code->verify_ip_address = $request->ip(); + $code->save(); + } + $has_domain = $this->getPlan()->hasDomain(); // Return user name and email/phone/voucher from the codes database, @@ -255,7 +261,7 @@ ]; } else { // Validate verification codes (again) - $v = $this->verify($request); + $v = $this->verify($request, false); if ($v->status() !== 200) { return $v; } @@ -338,9 +344,13 @@ $invitation->save(); } - // Remove the verification code + // Soft-delete the verification code, and store some more info with it if ($request->code) { - $request->code->delete(); + $request->code->user_id = $user->id; + $request->code->submit_ip_address = $request->ip(); + $request->code->deleted_at = \now(); + $request->code->timestamps = false; + $request->code->save(); } DB::commit(); diff --git a/src/app/SignupCode.php b/src/app/SignupCode.php --- a/src/app/SignupCode.php +++ b/src/app/SignupCode.php @@ -2,6 +2,7 @@ namespace App; +use App\Traits\BelongsToUserTrait; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -22,11 +23,14 @@ * @property ?string $plan Plan title * @property string $short_code Short validation code * @property \Carbon\Carbon $updated_at The update timestamp + * @property string $submit_ip_address IP address the final signup submit request came from + * @property string $verify_ip_address IP address the code verify request came from * @property ?string $voucher Voucher discount code */ class SignupCode extends Model { use SoftDeletes; + use BelongsToUserTrait; public const SHORTCODE_LENGTH = 5; public const CODE_LENGTH = 32; @@ -58,6 +62,9 @@ /** @var array The attributes that should be cast */ protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', 'expires_at' => 'datetime:Y-m-d H:i:s', 'headers' => 'array' ]; diff --git a/src/database/migrations/2022_10_10_100000_signup_codes_user_id.php b/src/database/migrations/2022_10_10_100000_signup_codes_user_id.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2022_10_10_100000_signup_codes_user_id.php @@ -0,0 +1,46 @@ +string('verify_ip_address')->index()->nullable(); + $table->string('submit_ip_address')->index()->nullable(); + + $table->bigInteger('user_id')->index()->nullable(); + $table->foreign('user_id')->references('id')->on('users') + ->onUpdate('cascade')->onDelete('cascade'); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table( + 'signup_codes', + function (Blueprint $table) { + $table->dropColumn('user_id'); + $table->dropColumn('verify_ip_address'); + $table->dropColumn('submit_ip_address'); + } + ); + } +}; diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php --- a/src/tests/Feature/Controller/SignupTest.php +++ b/src/tests/Feature/Controller/SignupTest.php @@ -280,7 +280,7 @@ 'plan' => 'individual', ]; - $response = $this->post('/api/auth/signup/init', $data); + $response = $this->post('/api/auth/signup/init', $data, ['REMOTE_ADDR' => '10.1.1.2']); $json = $response->json(); $response->assertStatus(200); @@ -290,6 +290,12 @@ $this->assertSame('email', $json['mode']); $this->assertNotEmpty($json['code']); + $code = SignupCode::find($json['code']); + + $this->assertSame('10.1.1.2', $code->ip_address); + $this->assertSame(null, $code->verify_ip_address); + $this->assertSame(null, $code->submit_ip_address); + // Assert the email sending job was pushed once Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1); @@ -395,12 +401,14 @@ public function testSignupVerifyValidInput(array $result): array { $code = SignupCode::find($result['code']); + $code->ip_address = '10.1.1.2'; + $code->save(); $data = [ 'code' => $code->code, 'short_code' => $code->short_code, ]; - $response = $this->post('/api/auth/signup/verify', $data); + $response = $this->post('/api/auth/signup/verify', $data, ['REMOTE_ADDR' => '10.1.1.3']); $json = $response->json(); $response->assertStatus(200); @@ -413,6 +421,12 @@ $this->assertSame(false, $json['is_domain']); $this->assertTrue(is_array($json['domains']) && !empty($json['domains'])); + $code->refresh(); + + $this->assertSame('10.1.1.2', $code->ip_address); + $this->assertSame('10.1.1.3', $code->verify_ip_address); + $this->assertSame(null, $code->submit_ip_address); + return $result; } @@ -560,6 +574,9 @@ $domain = $this->getPublicDomain(); $identity = \strtolower('SignupLogin@') . $domain; $code = SignupCode::find($result['code']); + $code->ip_address = '10.1.1.2'; + $code->verify_ip_address = '10.1.1.3'; + $code->save(); $data = [ 'login' => 'SignupLogin', 'domain' => $domain, @@ -570,7 +587,7 @@ 'voucher' => 'TEST', ]; - $response = $this->post('/api/auth/signup', $data); + $response = $this->post('/api/auth/signup', $data, ['REMOTE_ADDR' => '10.1.1.4']); $json = $response->json(); $response->assertStatus(200); @@ -590,8 +607,7 @@ } ); - // Check if the code has been removed - $this->assertNull(SignupCode::where('code', $result['code'])->first()); + $code->refresh(); // Check if the user has been created $user = User::where('email', $identity)->first(); @@ -599,6 +615,13 @@ $this->assertNotEmpty($user); $this->assertSame($identity, $user->email); + // Check if the code has been updated and soft-deleted + $this->assertTrue($code->trashed()); + $this->assertSame('10.1.1.2', $code->ip_address); + $this->assertSame('10.1.1.3', $code->verify_ip_address); + $this->assertSame('10.1.1.4', $code->submit_ip_address); + $this->assertSame($user->id, $code->user_id); + // Check user settings $this->assertSame($result['first_name'], $user->getSetting('first_name')); $this->assertSame($result['last_name'], $user->getSetting('last_name'));