From 185282ff5484786dc89a1dfc8f3765c31410d13d Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 31 Jan 2026 14:07:48 +1030 Subject: [PATCH 1/5] fix: set chmod 755 on avatars-temp directory creation Fixes permission denied error on subsequent Docker builds --- app/Services/AvatarService.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Services/AvatarService.php b/app/Services/AvatarService.php index 4ecb2464..875a7d5e 100644 --- a/app/Services/AvatarService.php +++ b/app/Services/AvatarService.php @@ -113,7 +113,11 @@ private static function processImage($avatarFile, int $profileId): string $tempFileName = 'avatar-'.$profileId.'-'.time().'.jpg'; $tempPath = storage_path('app/avatars-temp/'.$tempFileName); - Storage::disk('local')->makeDirectory('avatars-temp'); + $dirPath = storage_path('app/avatars-temp'); + if (! is_dir($dirPath)) { + Storage::disk('local')->makeDirectory('avatars-temp'); + chmod($dirPath, 0755); + } Image::read($avatarFile) ->cover(300, 300) From feb2181e6a02b48d1d0274b1cbacfc742e8838a3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 31 Jan 2026 14:51:02 +1030 Subject: [PATCH 2/5] fix: streamline email verification flow after login - Keep user logged in when email verification is needed (no logout) - Auto-send verification code on login for unverified accounts - Skip to code entry step directly (no re-entering credentials) - Add /api/v1/auth/verify/email/status endpoint to check pending verification - Extract sendVerificationCode into reusable static method - Always resend existing code (handles spam/deleted emails) Fixes #420 --- app/Http/Controllers/Auth/LoginController.php | 33 ++++- .../EmailVerificationController.php | 117 +++++++++++++----- resources/js/components/AuthModal.vue | 2 + .../js/pages/auth/manuallyVerifyEmail.vue | 29 ++++- routes/api.php | 1 + 5 files changed, 148 insertions(+), 34 deletions(-) diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 5df0a75b..ddc208e8 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -135,15 +135,44 @@ protected function sendLoginResponse(Request $request) $user->update(['last_ip' => $request->ip()]); if (! $user->email_verified_at) { + // Automatically send verification code before logging out + $existing = \App\Models\UserRegisterVerify::whereEmail($user->email) + ->whereNull('verified_at') + ->latest() + ->first(); + + if (! $existing) { + $sessionKey = \Illuminate\Support\Str::random(32); + $reg = new \App\Models\UserRegisterVerify; + $reg->session_key = $sessionKey; + $reg->email = $user->email; + $reg->verify_code = (string) app(\App\Support\VerificationCode::class)->generate(); + $reg->ip_address = $request->ip(); + $reg->user_agent = $request->userAgent(); + $reg->email_last_sent_at = now(); + $reg->save(); + + $request->session()->put('user:reg:session_key', $sessionKey); + $request->session()->put('user:reg:email', $reg->email); + $request->session()->put('user:reg:id', $reg->id); + + \App\Jobs\Auth\NewAccountEmailVerifyJob::dispatch($reg); + } else { + $request->session()->put('user:reg:session_key', $existing->session_key); + $request->session()->put('user:reg:email', $existing->email); + $request->session()->put('user:reg:id', $existing->id); + } + $this->guard()->logout(); - $request->session()->invalidate(); - $request->session()->regenerateToken(); + $request->session()->regenerate(); if ($request->expectsJson() || $request->ajax()) { return response()->json([ 'redirect' => url('/auth/verify-email'), 'message' => 'You need to verify your email.', + 'email' => $user->email, + 'verification_sent' => true, ], 403); } diff --git a/app/Http/Controllers/EmailVerificationController.php b/app/Http/Controllers/EmailVerificationController.php index 9df93dda..ced51045 100644 --- a/app/Http/Controllers/EmailVerificationController.php +++ b/app/Http/Controllers/EmailVerificationController.php @@ -15,6 +15,54 @@ class EmailVerificationController extends Controller { + /** + * Send verification code for a user (can be called from other controllers) + * + * Checks if there's already a pending (unverified) code for this user. + * If true, resends the same code (in case original was lost/spam). + * If false, creates a new code and sends the verification email. + * + * @return bool True if a new code was created, false if existing code was resent + */ + public static function sendVerificationCode(User $user, Request $request): bool + { + // Check for existing pending verification code + $existing = UserRegisterVerify::whereEmail($user->email) + ->whereNull('verified_at') + ->latest() + ->first(); + + if ($existing) { + // Resend existing code (in case it was lost or caught in spam) + $request->session()->put('user:reg:session_key', $existing->session_key); + $request->session()->put('user:reg:email', $existing->email); + $request->session()->put('user:reg:id', $existing->id); + + NewAccountEmailVerifyJob::dispatch($existing); + + return false; + } + + // Create new verification code and send email + $sessionKey = Str::random(32); + $reg = new UserRegisterVerify; + $reg->session_key = $sessionKey; + $reg->email = $user->email; + $reg->verify_code = (string) app(VerificationCode::class)->generate(); + $reg->ip_address = $request->ip(); + $reg->user_agent = $request->userAgent(); + $reg->email_last_sent_at = now(); + $reg->save(); + + $request->session()->put('user:reg:session_key', $sessionKey); + $request->session()->put('user:reg:email', $reg->email); + $request->session()->put('user:reg:id', $reg->id); + + NewAccountEmailVerifyJob::dispatch($reg); + + return true; + } + /** * Initiate email verification - send code */ @@ -48,37 +96,7 @@ public function initiate(EmailManualVerificationRequest $request) ], 400); } - $existing = UserRegisterVerify::whereEmail($user->email) - ->whereNull('verified_at') - ->latest() - ->first(); - - if ($existing) { - $request->session()->put('user:reg:session_key', $existing->session_key); - $request->session()->put('user:reg:email', $existing->email); - $request->session()->put('user:reg:id', $existing->id); - - return response()->json([ - 'message' => 'We have already sent a verification code to your email.', - 'expires_in' => 900, - ]); - } - - $sessionKey = Str::random(32); - $reg = new UserRegisterVerify; - $reg->session_key = $sessionKey; - $reg->email = $user->email; - $reg->verify_code = (string) app(VerificationCode::class)->generate(); - $reg->ip_address = $request->ip(); - $reg->user_agent = $request->userAgent(); - $reg->email_last_sent_at = now(); - $reg->save(); - - $request->session()->put('user:reg:session_key', $sessionKey); - $request->session()->put('user:reg:email', $reg->email); - $request->session()->put('user:reg:id', $reg->id); - - NewAccountEmailVerifyJob::dispatch($reg); + self::sendVerificationCode($user, $request); return response()->json([ 'message' => 'Verification code sent to your email.', @@ -217,4 +235,41 @@ public function resend(Request $request) 'message' => 'New verification code sent to your email.', ]); } + + /** + * Check if there's a pending email verification session + */ + public function status(Request $request) + { + $sEmail = $request->session()->get('user:reg:email'); + $sKey = $request->session()->get('user:reg:session_key'); + $sId = $request->session()->get('user:reg:id'); + + if (! $sEmail || ! $sKey || ! $sId) { + return response()->json([ + 'has_pending_verification' => false, + ]); + } + + // Verify the session is still valid + $verify = UserRegisterVerify::where('session_key', $sKey) + ->whereNull('verified_at') + ->find($sId); + + if (! $verify) { + return response()->json([ + 'has_pending_verification' => false, + ]); + } + + // Mask the email for privacy (show first 2 chars and domain) + $emailParts = explode('@', $sEmail); + $maskedEmail = substr($emailParts[0], 0, 2).'***@'.$emailParts[1]; + + return response()->json([ + 'has_pending_verification' => true, + 'email' => $sEmail, + 'masked_email' => $maskedEmail, + ]); + } } diff --git a/resources/js/components/AuthModal.vue b/resources/js/components/AuthModal.vue index 3d1a278d..7c7ee518 100644 --- a/resources/js/components/AuthModal.vue +++ b/resources/js/components/AuthModal.vue @@ -1427,6 +1427,8 @@ const handleLogin = async () => { if (res.data.has_2fa) { currentMode.value = 'two-factor' setSuccess(t('common.pleaseEnterYour2FACode')) + } else if (res.data.verification_sent) { + window.location.href = '/auth/verify-email' } else if (res.data.redirect) { window.location.href = res.data.redirect } else { diff --git a/resources/js/pages/auth/manuallyVerifyEmail.vue b/resources/js/pages/auth/manuallyVerifyEmail.vue index 9131d6c7..b06d9b9d 100644 --- a/resources/js/pages/auth/manuallyVerifyEmail.vue +++ b/resources/js/pages/auth/manuallyVerifyEmail.vue @@ -4,6 +4,13 @@ class="grid min-h-full w-full place-items-center bg-white dark:bg-slate-950 px-6 py-24 sm:py-32 lg:px-8" >
+ +
+ +

{{ t('common.loading') }}...

+
+ +