-
Notifications
You must be signed in to change notification settings - Fork 7
Sponsor Login Controller #5372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
LightningBoltz21
wants to merge
43
commits into
main
Choose a base branch
from
logincontroller
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Sponsor Login Controller #5372
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
e47b82a
login controller progress
LightningBoltz21 e4276ac
Merge sponsor_user branch - resolve composer.json conflict
LightningBoltz21 1851139
removed unnecessary code, integrated spatie
LightningBoltz21 6dc07c2
Merge remote-tracking branch 'origin/main' into logincontroller
LightningBoltz21 154aa62
Refactor SponsorLoginController to handle new user creation during OT…
LightningBoltz21 2a75938
modified controller to work with UI
LightningBoltz21 63cf18d
modified view to work with controller, added routes to web.php
LightningBoltz21 b5293d4
fixed styling issues
LightningBoltz21 06555f1
WIP: save current state before restore
LightningBoltz21 e6c4c91
fixed more styling issues
LightningBoltz21 3c8487e
fixed formatting errors
LightningBoltz21 7990046
Fix formatting errors in SponsorLoginController
LightningBoltz21 8354db2
fixed phan styling errors
LightningBoltz21 397f6ff
Fix CI issues
kberzinch a9db97f
Update app/Http/Controllers/SponsorLoginController.php
LightningBoltz21 174344c
saving sponsorUser before authentication
LightningBoltz21 078e17e
formatting + session regeneration fix
LightningBoltz21 71d4955
fixed Auth login implmentation
LightningBoltz21 2b911b9
fixed requested changes, added logic that I believe will make sponsor…
jvogt23 b0f62ba
Update sponsor auth check
jvogt23 f6cfd5c
Linter appeasement
jvogt23 0f3bb77
Merge branch 'main' into logincontroller
jvogt23 a571c40
Linter appeasement
jvogt23 fd7e7ce
bug fix
jvogt23 c7daacd
Add sponsor ID to new sponsor user on create
jvogt23 ad5ceeb
Linter appeasement
jvogt23 4281f01
Linter appeasement
jvogt23 767b946
Add import
jvogt23 f530a1a
Fetch ID correctly
jvogt23 57a3b64
Make SponsorUser extend Illuminate Foundation User
jvogt23 da48619
Make SponsorUser notifiable
jvogt23 2ada41a
Bugfix
jvogt23 d86a899
Force build
jvogt23 262dfa5
Check login middleware
jvogt23 b5e1986
changed predicate in SponsorAuthCheck
jvogt23 f546fb8
Remove model check as it likely will not work
jvogt23 21adb97
Add uid attribute
jvogt23 39ede66
Remove SponsorAuthCheck
jvogt23 d9ba74d
Style fixes
jvogt23 d0c7d89
Merge branch 'main' into logincontroller
jvogt23 95d5b12
linter appeasement
jvogt23 8baa773
Merge branch 'logincontroller' of github.com:Robojackets/apiary into …
jvogt23 ec55bc0
Add sponsor_id to phpdoc
jvogt23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Http\Controllers; | ||
|
|
||
| use App\Models\Sponsor; | ||
| use App\Models\SponsorDomain; | ||
| use App\Models\SponsorUser; | ||
| use Illuminate\Http\JsonResponse; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Auth; | ||
|
|
||
| class SponsorLoginController | ||
| { | ||
| public function showLoginForm() | ||
| { | ||
| return view('sponsors.login'); | ||
| } | ||
|
|
||
| public function validateEmail(Request $request): JsonResponse | ||
| { | ||
| // Validate input request using Laravel's in-built validator | ||
| $request->validate([ | ||
| 'email' => [ | ||
| 'required', | ||
| 'string', | ||
| 'email:rfc,strict,dns,spoof', | ||
| 'max:255', | ||
| ], | ||
| ]); | ||
|
|
||
| // Read value - cast to string since validation guarantees it | ||
| $email = (string) $request->input('email'); | ||
|
|
||
| // Check if domain is valid and sponsor is active; if not, return JSON error | ||
| if (! $this->isValidSponsorDomain($email)) { | ||
| return $this->errorResponse( | ||
| 'Authentication Error', | ||
| 'Could not validate email or sponsor is no longer active. '. | ||
| 'Contact [email protected] if the issue persists.' | ||
| ); | ||
| } | ||
|
|
||
| $sponsorUser = SponsorUser::where('email', $email)->first(); | ||
| if (! $sponsorUser) { | ||
| $sponsorUser = new SponsorUser(); | ||
| $sponsorUser->email = $email; | ||
| $sponsorUser->sponsor_id = Sponsor::whereHas( | ||
| 'domainNames', | ||
| static fn ($q) => $q->where('domain_name', substr(strrchr($email, '@'), 1)) | ||
| ) | ||
| ->firstOrFail() | ||
| ->id; | ||
| $sponsorUser->save(); | ||
| } | ||
|
|
||
| // Generate and dispatch OTP using Spatie | ||
| $sponsorUser->sendOneTimePassword(); | ||
|
|
||
| // Cache minimal state for OTP verification | ||
| session([ | ||
| 'sponsor_email_pending' => $email, | ||
| ]); | ||
|
|
||
| return response()->json([ | ||
| 'success' => true, | ||
| 'message' => 'One-Time Password Sent! Please type the one-time password sent to your email.', | ||
| ], 200); | ||
| } | ||
|
|
||
| public function verifyOtp(Request $request): JsonResponse | ||
| { | ||
| // Laravel will automatically throw an error if OTP is invalid | ||
| $request->validate([ | ||
| 'otp' => ['required', 'string', 'digits:6'], | ||
| ]); | ||
|
|
||
| // Validate session and retrieve user | ||
| $email = session('sponsor_email_pending'); | ||
| if (! is_string($email) || $email === '') { | ||
| return $this->errorResponse( | ||
| 'Session Expired', | ||
| 'Your session has expired. Please start the login process again.' | ||
| ); | ||
| } | ||
|
|
||
| // Retrieve existing user for OTP verification | ||
| // sole() ensures exactly one user exists (throws exception if 0 or >1 found) | ||
| $sponsorUser = SponsorUser::where('email', $email)->sole(); | ||
|
|
||
| // Verify sponsor domain is still valid and active BEFORE verifying OTP | ||
| if (! $this->isValidSponsorDomain($email)) { | ||
| return $this->errorResponse( | ||
| 'Authentication Error', | ||
| 'Could not validate email or sponsor is no longer active. '. | ||
| 'Please contact [email protected] if the issue persists.' | ||
| ); | ||
| } | ||
|
|
||
| // Verify OTP using Spatie | ||
| $otp = (string) $request->input('otp'); | ||
| $result = $sponsorUser->attemptLoginUsingOneTimePassword($otp); | ||
| if (! $result->isOk()) { | ||
| return $this->errorResponse('Invalid OTP', $result->validationMessage()); | ||
| } | ||
|
|
||
| Auth::guard('sponsor')->login($sponsorUser); | ||
|
|
||
| session()->forget('sponsor_email_pending'); | ||
|
|
||
| return response()->json([ | ||
| 'success' => true, | ||
| 'message' => 'Login successful! Redirecting to dashboard...', | ||
| 'redirect' => route('sponsor.home'), | ||
| ]); | ||
| } | ||
|
|
||
| private function isValidSponsorDomain(string $email): bool | ||
| { | ||
| $domain = substr(strrchr($email, '@'), 1); | ||
| $sponsorDomain = SponsorDomain::where('domain_name', $domain)->first(); | ||
|
|
||
| if (! $sponsorDomain) { | ||
| return false; | ||
| } | ||
|
|
||
| return $sponsorDomain->sponsor && $sponsorDomain->sponsor->active(); | ||
| } | ||
|
|
||
| private function errorResponse(string $title, string $message, int $status = 422): JsonResponse | ||
| { | ||
| // Dummy comment to force a build on Github site. | ||
| return response()->json([ | ||
| 'error' => true, | ||
| 'title' => $title, | ||
| 'message' => $message, | ||
| ], $status); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,7 +48,7 @@ | |
| <button | ||
| class="btn btn-link" | ||
| :disabled="!canResend" | ||
| @click="sendOTP" | ||
| @click="validateEmail" | ||
| > | ||
| {{ canResend ? 'Resend OTP' : `Resend in ${resendCooldown}s` }} | ||
| </button> | ||
|
|
@@ -73,6 +73,10 @@ export default { | |
| resendCooldown: 30, | ||
| }; | ||
| }, | ||
| beforeUnmount() { | ||
| // Clean up interval timer when app is exited to prevent memory leaks | ||
| clearInterval(this.resendTimer); | ||
| }, | ||
| methods: { | ||
| showToast(message, icon = 'info') { | ||
| Swal.fire({ | ||
|
|
@@ -85,30 +89,42 @@ export default { | |
| timerProgressBar: true | ||
| }); | ||
| }, | ||
| handleApiError(error, validationField = null) { | ||
| if (error.response?.data) { | ||
| const data = error.response.data; | ||
|
|
||
| // Custom error response from controller | ||
| if (data.error && data.title && data.message) { | ||
| Swal.fire(data.title, data.message, 'error'); | ||
| } | ||
| // Laravel validation error | ||
| else if (validationField && data.errors?.[validationField]) { | ||
| Swal.fire('Validation Error', data.errors[validationField][0], 'error'); | ||
| } | ||
| // Any other server error | ||
| else { | ||
| Swal.fire('Error', 'Something went wrong. Please try again. If the issue persists, contact [email protected].', 'error'); | ||
| } | ||
| } else { | ||
| // Network error (no response from server) | ||
| Swal.fire('Connection Error', 'Unable to reach the server. Please check your connection. If the issue persists, contact [email protected].', 'error'); | ||
| } | ||
| }, | ||
| validateEmail() { | ||
| // axios.post('/sponsor/check-email', { email: this.email }) | ||
| // .then(res => { | ||
| // if (res.data.valid) { | ||
| // this.emailValidated = true; | ||
| // this.showToast('A one-time password has been sent to your email.', 'info'); | ||
| // } else { | ||
| // this.showToast('Email domain not approved.', 'error'); | ||
| // } | ||
| // }) | ||
| // .catch(() => { | ||
| // this.showToast('Something went wrong. Please try again.', 'error'); | ||
| // }); | ||
| if (this.email === '[email protected]') { | ||
| axios.post('/sponsor/validate-email', { email: this.email }) | ||
| .then(res => { | ||
| if (res.data.success) { // else need to throw error | ||
| this.emailValidated = true; | ||
| this.sendOTP(); | ||
| } else { | ||
| // NOTE: [email protected] is a placeholder for now; will change when I find out | ||
| // who is a good point of contact | ||
| Swal.fire('Authentication Error', 'Could not validate email domain. Please try again, or contact <a href="[email protected]">[email protected]</a> if issues persist.', 'error'); | ||
| } | ||
| this.beginResendCooldown(); | ||
|
|
||
| } | ||
| }) | ||
| .catch(error => { | ||
| this.handleApiError(error, 'email'); | ||
| }); | ||
| }, | ||
| beginResendCooldown() { | ||
| this.resendCooldown = 30; | ||
| this.resendCooldown = 60; // may want to increase to 2 mins in future | ||
| this.canResend = false; | ||
| clearInterval(this.resendTimer); | ||
| this.resendTimer = setInterval(() => { | ||
|
|
@@ -120,30 +136,18 @@ export default { | |
| } | ||
| }, 1000); | ||
| }, | ||
| sendOTP() { | ||
| //TODO: OTP Logic. Rate-limiting should be implemented on backend. | ||
| this.beginResendCooldown(); | ||
| }, | ||
| handleSubmit() { | ||
| // axios.post('/sponsor/login', { email: this.email, password: this.password }) | ||
| // .then(res => { | ||
| // if (res.data.success) { | ||
| // window.location.href = '/sponsor/dashboard'; | ||
| // } else { | ||
| // this.showToast('Invalid password.', 'error'); | ||
| // } | ||
| // }) | ||
| // .catch(() => { | ||
| // this.showToast('Something went wrong. Please try again.', 'error'); | ||
| // }); | ||
| if (this.password === 'hello') { | ||
| window.location.href='/'; | ||
| } else { | ||
| Swal.fire( | ||
| 'Authentication Error', | ||
| 'Could not validate password. Please try again or contact <a href="[email protected]">[email protected]</a> if the issue persists.', | ||
| 'error'); | ||
| } | ||
| // TODO: ensure correct route | ||
| axios.post('/sponsor/verify-otp', { otp: this.password }) | ||
| .then(res => { | ||
| if (res.data.success) { | ||
| // Redirect to the URL provided by the controller | ||
| window.location.href = res.data.redirect; | ||
| } | ||
| }) | ||
| .catch(error => { | ||
| this.handleApiError(error, 'otp'); | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| Hello, | ||
|
|
||
| You requested a one-time password to log in to the RoboJackets Sponsor Portal. | ||
|
|
||
| Your one-time password is: {{ $otp }} | ||
|
|
||
| This password will expire in 10 minutes. | ||
|
|
||
| If you did not request this password, please contact [email protected]. | ||
LightningBoltz21 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ---- | ||
|
|
||
| To stop receiving emails from {{ config('app.name') }}, visit @{{{ pm:unsubscribe }}}. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <title>Hello World</title> | ||
| </head> | ||
| <body> | ||
| Hello World | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.