From 558c3ab9ff3e784db37c80707b7bca844a7ec44f Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 28 Aug 2026 14:01:41 +0600 Subject: [PATCH 1/4] refactor(auth): move AuthController from Admin to root Controllers namespace --- app/Http/Controllers/{Admin => }/AuthController.php | 2 +- routes/web.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/Http/Controllers/{Admin => }/AuthController.php (98%) diff --git a/app/Http/Controllers/Admin/AuthController.php b/app/Http/Controllers/AuthController.php similarity index 98% rename from app/Http/Controllers/Admin/AuthController.php rename to app/Http/Controllers/AuthController.php index ce3230ac..351b07b3 100644 --- a/app/Http/Controllers/Admin/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -1,6 +1,6 @@ Date: Fri, 28 Aug 2026 14:01:52 +0600 Subject: [PATCH 2/4] feat(auth): implement user onboarding screen and profile setup flow after Google auth --- app/Http/Controllers/AuthController.php | 95 ++++++-- resources/js/pages/auth/Login.vue | 6 +- resources/js/pages/auth/Onboarding.vue | 298 ++++++++++++++++++++++++ routes/web.php | 2 + 4 files changed, 384 insertions(+), 17 deletions(-) create mode 100644 resources/js/pages/auth/Onboarding.vue diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 351b07b3..9b42ce1c 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -8,6 +8,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Str; use Inertia\Inertia; use Laravel\Socialite\Facades\Socialite; @@ -59,25 +60,97 @@ public function handleGoogleCallback(Request $request) ->orWhere('email', $googleUser->getEmail()) ->first(); - $isNewUser = false; - if ($user) { $user->update([ 'google_id' => $googleUser->getId(), 'email_verified_at' => $user->email_verified_at ?? now(), ]); - } else { + + Auth::login($user, remember: true); + $request->session()->regenerate(); + + $defaultUrl = $user->username + ? route('user.profile', $user->username) + : route('profile.edit'); + + return redirect()->intended($defaultUrl); + } + + $request->session()->put('onboarding_user', [ + 'google_id' => $googleUser->getId(), + 'email' => $googleUser->getEmail(), + 'name' => $googleUser->getName() ?? $googleUser->getNickname() ?? '', + 'avatar' => $googleUser->getAvatar() ?? null, + ]); + + return redirect()->route('onboarding'); + } + + public function showOnboarding(Request $request) + { + if (Auth::check()) { + return redirect()->route('index'); + } + + if (! $request->session()->has('onboarding_user')) { + return redirect()->route('login')->with('error', 'Please continue with Google to create an account.'); + } + + $onboardingUser = $request->session()->get('onboarding_user'); + + return Inertia::render('auth/Onboarding', [ + 'user' => $onboardingUser, + ]); + } + + public function completeOnboarding(Request $request) + { + if (Auth::check()) { + return redirect()->route('index'); + } + + if (! $request->session()->has('onboarding_user')) { + return redirect()->route('login')->with('error', 'Session expired. Please continue with Google again.'); + } + + $onboardingData = $request->session()->get('onboarding_user'); + + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'username' => [ + 'required', + 'string', + 'min:3', + 'max:30', + 'regex:/^[a-zA-Z0-9_]+$/', + 'unique:users,username', + ], + 'school' => ['required', 'string', 'max:255'], + ], [ + 'school.required' => 'Please enter your school, college, or institution name.', + 'username.regex' => 'Username can only contain letters, numbers, and underscores.', + 'username.unique' => 'This username is already taken. Please choose another one.', + ]); + + $user = User::where('google_id', $onboardingData['google_id']) + ->orWhere('email', $onboardingData['email']) + ->first(); + + if (! $user) { $user = User::create([ - 'name' => $googleUser->getName() ?? $googleUser->getNickname() ?? 'Google User', - 'email' => $googleUser->getEmail(), - 'google_id' => $googleUser->getId(), + 'name' => $validated['name'], + 'username' => $validated['username'], + 'email' => $onboardingData['email'], + 'google_id' => $onboardingData['google_id'], + 'institution' => $validated['school'], 'email_verified_at' => now(), ]); - $isNewUser = true; Mail::to($user->email)->queue(new WelcomeUserMail($user)); } + $request->session()->forget('onboarding_user'); + Auth::login($user, remember: true); $request->session()->regenerate(); @@ -85,13 +158,7 @@ public function handleGoogleCallback(Request $request) ? route('user.profile', $user->username) : route('profile.edit'); - $redirect = redirect()->intended($defaultUrl); - - if ($isNewUser) { - $redirect->with('success', 'Account not found. New account created.'); - } - - return $redirect; + return redirect()->intended($defaultUrl)->with('success', 'Account created successfully! Welcome to HSCStack.'); } public function logout(Request $request) diff --git a/resources/js/pages/auth/Login.vue b/resources/js/pages/auth/Login.vue index dac62a37..72b16a8b 100644 --- a/resources/js/pages/auth/Login.vue +++ b/resources/js/pages/auth/Login.vue @@ -141,10 +141,10 @@ const googleAuthUrl = redirectParam > Instant 1-Click Access:Fast Google Access: - নতুন ইউজার হলে এক ক্লিকেই auto account create - হবে, আর পুরাতন ইউজার হলে সরাসরি Login হয়ে যাবে। + নতুন ইউজার হলে তথ্য সেটআপ করে অ্যাকাউন্ট তৈরি + করতে পারবেন, আর পুরাতন ইউজার হলে সরাসরি Login হয়ে যাবে। diff --git a/resources/js/pages/auth/Onboarding.vue b/resources/js/pages/auth/Onboarding.vue new file mode 100644 index 00000000..09e09b8e --- /dev/null +++ b/resources/js/pages/auth/Onboarding.vue @@ -0,0 +1,298 @@ + + + diff --git a/routes/web.php b/routes/web.php index 5ac9c2f6..de5cd748 100644 --- a/routes/web.php +++ b/routes/web.php @@ -72,6 +72,8 @@ Route::post('/logout', [AuthController::class, 'logout'])->name('logout'); Route::get('/auth/google', [AuthController::class, 'redirectToGoogle'])->name('auth.google'); Route::get('/auth/google/callback', [AuthController::class, 'handleGoogleCallback'])->name('auth.google.callback'); + Route::get('/onboarding', [AuthController::class, 'showOnboarding'])->name('onboarding'); + Route::post('/onboarding', [AuthController::class, 'completeOnboarding'])->name('onboarding.complete'); Route::get('/blogs', [BlogController::class, 'index']); Route::get('/blogs/{blog}', [BlogController::class, 'show']); From 645a468364bea5ea334119e9090cd4eaa8755ce0 Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 28 Aug 2026 14:01:55 +0600 Subject: [PATCH 3/4] test(auth): add feature tests for onboarding flow and redirection --- tests/Feature/AuthenticationTest.php | 162 +++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 11 deletions(-) diff --git a/tests/Feature/AuthenticationTest.php b/tests/Feature/AuthenticationTest.php index 374ccc04..4dfc3efd 100644 --- a/tests/Feature/AuthenticationTest.php +++ b/tests/Feature/AuthenticationTest.php @@ -1,6 +1,8 @@ assertStringContainsString('accounts.google.com', $response->headers->get('Location')); }); -test('google auth creates new user account if not exists and flashes notice', function () { +test('google auth transfers new user to onboarding without creating account immediately', function () { $abstractUser = Mockery::mock(Laravel\Socialite\Two\User::class); $abstractUser->shouldReceive('getId')->andReturn('google-id-12345'); $abstractUser->shouldReceive('getEmail')->andReturn('newuser@example.com'); $abstractUser->shouldReceive('getName')->andReturn('Google User'); $abstractUser->shouldReceive('getNickname')->andReturn('googleuser'); + $abstractUser->shouldReceive('getAvatar')->andReturn('https://example.com/avatar.jpg'); Socialite::shouldReceive('driver->user')->andReturn($abstractUser); $response = $this->get(route('auth.google.callback')); - $user = User::where('email', 'newuser@example.com')->first(); - $response->assertRedirect(route('user.profile', $user->username)); - $response->assertSessionHas('success', 'Account not found. New account created.'); - $this->assertAuthenticated(); - $this->assertDatabaseHas('users', [ + $response->assertRedirect(route('onboarding')); + $this->assertGuest(); + $this->assertDatabaseMissing('users', [ 'email' => 'newuser@example.com', - 'google_id' => 'google-id-12345', - 'name' => 'Google User', ]); + $response->assertSessionHas('onboarding_user', function ($data) { + return $data['email'] === 'newuser@example.com' + && $data['google_id'] === 'google-id-12345' + && $data['name'] === 'Google User' + && $data['avatar'] === 'https://example.com/avatar.jpg'; + }); +}); + +test('onboarding page is accessible with onboarding session', function () { + $response = $this->withSession([ + 'onboarding_user' => [ + 'google_id' => 'google-id-12345', + 'email' => 'newuser@example.com', + 'name' => 'Google User', + 'avatar' => null, + ], + ])->get(route('onboarding')); + + $response->assertStatus(200); +}); + +test('onboarding page redirects to login without onboarding session', function () { + $response = $this->get(route('onboarding')); + + $response->assertRedirect(route('login')); + $response->assertSessionHas('error', 'Please continue with Google to create an account.'); +}); + +test('completing onboarding creates user, queues welcome mail, and logs in', function () { + Mail::fake(); + + $response = $this->withSession([ + 'onboarding_user' => [ + 'google_id' => 'google-id-12345', + 'email' => 'newuser@example.com', + 'name' => 'Google User', + 'avatar' => null, + ], + ])->post(route('onboarding.complete'), [ + 'name' => 'Custom Name', + 'username' => 'custom_handle', + 'school' => 'Notre Dame College', + ]); + + $user = User::where('email', 'newuser@example.com')->first(); + $this->assertNotNull($user); + $this->assertEquals('Custom Name', $user->name); + $this->assertEquals('custom_handle', $user->username); + $this->assertEquals('Notre Dame College', $user->institution); + $this->assertEquals('google-id-12345', $user->google_id); + $this->assertNotNull($user->email_verified_at); + + $this->assertAuthenticatedAs($user); + $response->assertRedirect(route('user.profile', 'custom_handle')); + $response->assertSessionHas('success'); + $response->assertSessionMissing('onboarding_user'); + + Mail::assertQueued(WelcomeUserMail::class, function ($mail) { + return $mail->hasTo('newuser@example.com'); + }); +}); + +test('completing onboarding validates username uniqueness and format', function () { + User::factory()->create([ + 'username' => 'taken_handle', + ]); + + $response = $this->withSession([ + 'onboarding_user' => [ + 'google_id' => 'google-id-12345', + 'email' => 'newuser@example.com', + 'name' => 'Google User', + 'avatar' => null, + ], + ])->post(route('onboarding.complete'), [ + 'name' => 'Custom Name', + 'username' => 'taken_handle', + 'school' => 'Dhaka College', + ]); + + $response->assertSessionHasErrors(['username']); + $this->assertGuest(); +}); + +test('completing onboarding requires school field', function () { + $response = $this->withSession([ + 'onboarding_user' => [ + 'google_id' => 'google-id-12345', + 'email' => 'newuser@example.com', + 'name' => 'Google User', + 'avatar' => null, + ], + ])->post(route('onboarding.complete'), [ + 'name' => 'Custom Name', + 'username' => 'valid_handle', + 'school' => '', + ]); + + $response->assertSessionHasErrors(['school']); + $this->assertGuest(); +}); + +test('redirects to custom redirect url after onboarding for new user', function () { + $this->get('/auth/google?redirect=/ai'); + + $abstractUser = Mockery::mock(Laravel\Socialite\Two\User::class); + $abstractUser->shouldReceive('getId')->andReturn('google-id-new-redirect'); + $abstractUser->shouldReceive('getEmail')->andReturn('new-redirect@example.com'); + $abstractUser->shouldReceive('getName')->andReturn('New Redirect User'); + $abstractUser->shouldReceive('getNickname')->andReturn('newredirect'); + $abstractUser->shouldReceive('getAvatar')->andReturn(null); + + Socialite::shouldReceive('driver->user')->andReturn($abstractUser); + + $callbackResponse = $this->get(route('auth.google.callback')); + $callbackResponse->assertRedirect(route('onboarding')); + + // Complete onboarding with intended URL in session + $onboardResponse = $this->post(route('onboarding.complete'), [ + 'name' => 'New Redirect User', + 'username' => 'new_redirect_user', + 'school' => 'Dhaka College', + ]); + + $onboardResponse->assertRedirect(url('/ai')); }); -test('redirects to custom redirect url after authentication if provided', function () { +test('redirects to custom redirect url after authentication for existing user', function () { + $user = User::factory()->create([ + 'email' => 'redirect-test@example.com', + 'google_id' => 'google-id-custom', + ]); + $this->get('/auth/google?redirect=/ai'); $abstractUser = Mockery::mock(Laravel\Socialite\Two\User::class); @@ -54,9 +183,15 @@ $response->assertRedirect(url('/ai')); }); -test('redirects to trusted subdomain after authentication if provided', function () { +test('redirects to trusted subdomain after authentication for existing user', function () { config(['app.url' => 'https://hscstack.site']); $subdomainUrl = 'https://ssc2026.hscstack.site'; + + $user = User::factory()->create([ + 'email' => 'subdomain@example.com', + 'google_id' => 'google-id-subdomain', + ]); + $this->get('/auth/google?redirect='.urlencode($subdomainUrl)); $abstractUser = Mockery::mock(Laravel\Socialite\Two\User::class); @@ -101,7 +236,12 @@ $response->assertSessionHas('error', 'Failed to authenticate with Google. Please try again.'); }); -test('google auth redirects to intended url if set', function () { +test('google auth redirects to intended url if set for existing user', function () { + $user = User::factory()->create([ + 'email' => 'intended@example.com', + 'google_id' => 'google-id-intended', + ]); + $abstractUser = Mockery::mock(Laravel\Socialite\Two\User::class); $abstractUser->shouldReceive('getId')->andReturn('google-id-intended'); $abstractUser->shouldReceive('getEmail')->andReturn('intended@example.com'); From 969232395a5dd52fdf06e06805b95f6affc48592 Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 28 Aug 2026 14:03:46 +0600 Subject: [PATCH 4/4] style: apply prettier code formatting --- resources/js/pages/auth/Login.vue | 3 ++- resources/js/pages/auth/Onboarding.vue | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/js/pages/auth/Login.vue b/resources/js/pages/auth/Login.vue index 72b16a8b..e8a36597 100644 --- a/resources/js/pages/auth/Login.vue +++ b/resources/js/pages/auth/Login.vue @@ -144,7 +144,8 @@ const googleAuthUrl = redirectParam >Fast Google Access: নতুন ইউজার হলে তথ্য সেটআপ করে অ্যাকাউন্ট তৈরি - করতে পারবেন, আর পুরাতন ইউজার হলে সরাসরি Login হয়ে যাবে। + করতে পারবেন, আর পুরাতন ইউজার হলে সরাসরি Login + হয়ে যাবে। diff --git a/resources/js/pages/auth/Onboarding.vue b/resources/js/pages/auth/Onboarding.vue index 09e09b8e..f304ff29 100644 --- a/resources/js/pages/auth/Onboarding.vue +++ b/resources/js/pages/auth/Onboarding.vue @@ -116,7 +116,9 @@ const submit = () => { class="h-3.5 w-3.5 shrink-0 text-emerald-500" /> -

+

Verified via Google

@@ -208,7 +210,8 @@ const submit = () => { for="school" class="mb-1.5 block text-xs font-semibold text-slate-700 dark:text-gray-300" > - School / College / Institution * + School / College / Institution + *