feat: add app and database modules

This commit is contained in:
2026-05-21 16:05:11 +07:00
parent 37b7e783f5
commit fad70d096b
212 changed files with 23901 additions and 0 deletions
@@ -0,0 +1,134 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Mail\TwoFactorOtp;
use App\Models\UserTrustedDevice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class TwoFactorController extends Controller
{
public function index()
{
if (! Session::has('auth.2fa_user_id')) {
return redirect()->route('login');
}
// Check if device is already trusted (Redirect to verify immediately with cookie token)
$userId = Session::get('auth.2fa_user_id');
$deviceId = request()->cookie('2fa_trust_device');
if ($deviceId && str_contains($deviceId, '|')) {
$parts = explode('|', $deviceId, 2);
if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
return view('auth.two-factor');
}
[$uuid, $secret] = $parts;
$trust = UserTrustedDevice::where('user_id', $userId)
->where('device_id', $uuid)
->where('expires_at', '>', now())
->first();
if ($trust && hash_equals($trust->token, hash('sha256', $secret))) {
// Auto login and skip 2FA view
$remember = Session::get('auth.2fa_remember', false);
Auth::loginUsingId($userId, $remember);
Session::forget(['auth.2fa_user_id', 'auth.2fa_code', 'auth.2fa_expires_at', 'auth.2fa_remember']);
session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
}
return view('auth.two-factor');
}
public function verify(Request $request)
{
$request->validate([
'code' => 'required|string|size:6',
'trust_device' => 'nullable|boolean',
]);
$userId = Session::get('auth.2fa_user_id');
$storedCode = Session::get('auth.2fa_code');
$expiresAt = Session::get('auth.2fa_expires_at');
if (! $userId || ! $storedCode || ! hash_equals((string) $storedCode, (string) $request->code)) {
return back()->with('error', __('Invalid verification code.'));
}
if (! $expiresAt || now()->timestamp > $expiresAt) {
Session::forget(['auth.2fa_user_id', 'auth.2fa_code', 'auth.2fa_expires_at', 'auth.2fa_remember']);
return redirect()->route('login')->with('error', __('Verification code has expired. Please log in again.'));
}
// Handle Trust Device
if ($request->boolean('trust_device')) {
$this->issueTrustToken($userId);
}
// Login user
$remember = Session::get('auth.2fa_remember', false);
Auth::loginUsingId($userId, $remember);
// Clear 2FA session then regenerate to prevent fixation
Session::forget(['auth.2fa_user_id', 'auth.2fa_code', 'auth.2fa_expires_at', 'auth.2fa_remember']);
session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
protected function issueTrustToken($userId)
{
$deviceId = Str::uuid();
$token = Str::random(64);
$days = get_setting('two_factor_trust_days', 30);
UserTrustedDevice::create([
'user_id' => $userId,
'device_id' => $deviceId,
'token' => hash('sha256', $token),
'expires_at' => now()->addDays($days),
]);
// Queue cookie with both UUID and Secret
cookie()->queue(
'2fa_trust_device',
$deviceId.'|'.$token,
$days * 24 * 60,
null, null, true, true // secure, httpOnly
);
}
public static function generateAndSendOtp($user)
{
$otp = str_pad((string) (hexdec(bin2hex(random_bytes(3))) % 1000000), 6, '0', STR_PAD_LEFT);
$expiresAt = now()->addMinutes(10)->timestamp;
Session::put('auth.2fa_user_id', $user->id);
Session::put('auth.2fa_code', $otp);
Session::put('auth.2fa_expires_at', $expiresAt);
try {
$request = request();
Mail::to($user->email)->send(new TwoFactorOtp(
otp: $otp,
userName: $user->name,
ipAddress: $request->ip(),
userAgent: $request->userAgent(),
));
} catch (\Exception $e) {
\Log::error('Failed to send 2FA Email: '.$e->getMessage());
}
session()->flash('info', __('Verification code has been sent to your email.'));
}
}