Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e47b82a
login controller progress
LightningBoltz21 Oct 11, 2025
e4276ac
Merge sponsor_user branch - resolve composer.json conflict
LightningBoltz21 Oct 16, 2025
1851139
removed unnecessary code, integrated spatie
LightningBoltz21 Oct 18, 2025
6dc07c2
Merge remote-tracking branch 'origin/main' into logincontroller
LightningBoltz21 Oct 18, 2025
154aa62
Refactor SponsorLoginController to handle new user creation during OT…
LightningBoltz21 Oct 18, 2025
2a75938
modified controller to work with UI
LightningBoltz21 Oct 24, 2025
63cf18d
modified view to work with controller, added routes to web.php
LightningBoltz21 Oct 25, 2025
b5293d4
fixed styling issues
LightningBoltz21 Oct 25, 2025
06555f1
WIP: save current state before restore
LightningBoltz21 Oct 25, 2025
e6c4c91
fixed more styling issues
LightningBoltz21 Oct 25, 2025
3c8487e
fixed formatting errors
LightningBoltz21 Oct 29, 2025
7990046
Fix formatting errors in SponsorLoginController
LightningBoltz21 Oct 29, 2025
8354db2
fixed phan styling errors
LightningBoltz21 Oct 29, 2025
397f6ff
Fix CI issues
kberzinch Oct 29, 2025
a9db97f
Update app/Http/Controllers/SponsorLoginController.php
LightningBoltz21 Nov 15, 2025
174344c
saving sponsorUser before authentication
LightningBoltz21 Nov 15, 2025
078e17e
formatting + session regeneration fix
LightningBoltz21 Nov 15, 2025
71d4955
fixed Auth login implmentation
LightningBoltz21 Nov 15, 2025
2b911b9
fixed requested changes, added logic that I believe will make sponsor…
jvogt23 Nov 22, 2025
b0f62ba
Update sponsor auth check
jvogt23 Nov 22, 2025
f6cfd5c
Linter appeasement
jvogt23 Nov 22, 2025
0f3bb77
Merge branch 'main' into logincontroller
jvogt23 Nov 22, 2025
a571c40
Linter appeasement
jvogt23 Nov 22, 2025
fd7e7ce
bug fix
jvogt23 Nov 22, 2025
c7daacd
Add sponsor ID to new sponsor user on create
jvogt23 Nov 22, 2025
ad5ceeb
Linter appeasement
jvogt23 Nov 22, 2025
4281f01
Linter appeasement
jvogt23 Nov 22, 2025
767b946
Add import
jvogt23 Nov 22, 2025
f530a1a
Fetch ID correctly
jvogt23 Nov 22, 2025
57a3b64
Make SponsorUser extend Illuminate Foundation User
jvogt23 Nov 22, 2025
da48619
Make SponsorUser notifiable
jvogt23 Nov 22, 2025
2ada41a
Bugfix
jvogt23 Nov 22, 2025
d86a899
Force build
jvogt23 Dec 3, 2025
262dfa5
Check login middleware
jvogt23 Dec 3, 2025
b5e1986
changed predicate in SponsorAuthCheck
jvogt23 Dec 3, 2025
f546fb8
Remove model check as it likely will not work
jvogt23 Dec 3, 2025
21adb97
Add uid attribute
jvogt23 Dec 3, 2025
39ede66
Remove SponsorAuthCheck
jvogt23 Dec 4, 2025
d9ba74d
Style fixes
jvogt23 Dec 4, 2025
d0c7d89
Merge branch 'main' into logincontroller
jvogt23 Dec 4, 2025
95d5b12
linter appeasement
jvogt23 Dec 4, 2025
8baa773
Merge branch 'logincontroller' of github.com:Robojackets/apiary into …
jvogt23 Dec 4, 2025
ec55bc0
Add sponsor_id to phpdoc
jvogt23 Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions app/Http/Controllers/SponsorLoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

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('sponsor.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.'
);
}

// Get sponsor user and check if exists in one query
$sponsorUser = SponsorUser::where('email', $email)->first();
if (! $sponsorUser) {
// Create new SponsorUser model for new users
$sponsorUser = new SponsorUser();
$sponsorUser->email = $email;
$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
$result = $sponsorUser->attemptLoginUsingOneTimePassword((string) $request->input('otp'));
if (! $result->isOk()) {
return $this->errorResponse('Invalid OTP', $result->validationMessage());
}

// Save new user to database after successful OTP verification and sponsor check
if (! $sponsorUser->exists) {
$sponsorUser->save();
}

// Establish authenticated session using Laravel's Auth facade
$request->session()->regenerate();
Auth::login($sponsorUser);

// Store authentication timestamp (similar to CAS)
$request->session()->put('authenticationInstant', Cas::getAttribute('authenticationDate'));

session()->forget('sponsor_email_pending');

return response()->json([
'success' => true,
'message' => 'Login successful! Redirecting to dashboard...',
'redirect' => route('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
{
return response()->json([
'error' => true,
'title' => $title,
'message' => $message,
], $status);
}
}
6 changes: 1 addition & 5 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@
<directory name="app" />
</errorLevel>
</LessSpecificImplementedReturnType>
<ImplementedReturnTypeMismatch>
<errorLevel type="suppress">
<directory name="app" />
</errorLevel>
</ImplementedReturnTypeMismatch>
<InvalidReturnStatement>
<errorLevel type="suppress">
<directory name="app" />
Expand Down Expand Up @@ -439,6 +434,7 @@
<directory name="app/Policies/"/>
<directory name="database/seeders/"/>
<referencedMethod name="App\HorizonHealthCheck::__construct"/>
<referencedMethod name="App\Providers\AppServiceProvider::boot"/>
<referencedMethod name="App\Util\Sentry::tracesSampler"/>
</errorLevel>
</PossiblyUnusedMethod>
Expand Down
92 changes: 48 additions & 44 deletions resources/js/components/sponsors/SponsorLogin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<button
class="btn btn-link"
:disabled="!canResend"
@click="sendOTP"
@click="validateEmail"
>
{{ canResend ? 'Resend OTP' : `Resend in ${resendCooldown}s` }}
</button>
Expand All @@ -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({
Expand All @@ -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(() => {
Expand All @@ -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');
});
}
}
};
Expand Down
9 changes: 9 additions & 0 deletions resources/views/mail/sponsor-otp.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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].
7 changes: 7 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\Http\Controllers\RemoteAttendanceController;
use App\Http\Controllers\ResumeController;
use App\Http\Controllers\RsvpController;
use App\Http\Controllers\SponsorLoginController;
use App\Http\Controllers\SquareCheckoutController;
use App\Http\Controllers\SUMSController;
use App\Http\Controllers\TeamController;
Expand Down Expand Up @@ -129,3 +130,9 @@
Route::get('oauth/authorize', [AuthorizationController::class, 'authorize'])
->name('passport.authorizations.authorize')
->middleware('auth');

Route::prefix('sponsor')->name('sponsor.')->group(static function (): void {
Route::get('/login', [SponsorLoginController::class, 'showLoginForm'])->name('login');
Route::post('/validate-email', [SponsorLoginController::class, 'validateEmail'])->name('validate-email');
Route::post('/verify-otp', [SponsorLoginController::class, 'verifyOtp'])->name('verify-otp');
});
Loading