Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
419a373
Add Fortify
pushpak1300 Sep 2, 2025
4cfad5b
wip
pushpak1300 Sep 3, 2025
de92b27
Merge branch 'main' into feat/two-factor-auth
pushpak1300 Sep 3, 2025
0357084
Support Fortify Options
pushpak1300 Sep 3, 2025
d965439
FIx Tests
pushpak1300 Sep 4, 2025
e72ce33
Add Tests
pushpak1300 Sep 5, 2025
6172b74
Test Fix
pushpak1300 Sep 5, 2025
f5c6063
Refactor Component
pushpak1300 Sep 5, 2025
3609fd8
Refactor Code
pushpak1300 Sep 8, 2025
2683ee4
formatting
pushpak1300 Sep 8, 2025
07c2414
Refactoring
pushpak1300 Sep 9, 2025
41e079a
Fix test
pushpak1300 Sep 9, 2025
da7d298
Formatting
pushpak1300 Sep 9, 2025
9741f78
formatting
pushpak1300 Sep 9, 2025
49acd68
formatting
pushpak1300 Sep 9, 2025
f33fa41
refactoring
pushpak1300 Sep 9, 2025
ac5fa58
Refactor input-otp-component it to use the $nextTick
pushpak1300 Sep 9, 2025
d919841
simplify input-otp component
pushpak1300 Sep 9, 2025
567f2c4
Change conditional signature for better clarity
pushpak1300 Sep 10, 2025
9355369
Merge branch 'main' into feat/two-factor-auth
pushpak1300 Sep 15, 2025
002c3df
Refactor input-otp component for improved clarity
pushpak1300 Sep 16, 2025
e393696
Refactor two-factor authentication views for improved clarity and fun…
pushpak1300 Sep 16, 2025
3252948
Add error handling in the components
pushpak1300 Sep 17, 2025
78b9971
Refactor confirm password functionality to use fortify confirm password
pushpak1300 Sep 17, 2025
c5d5f9c
Use regex instead of checking number
pushpak1300 Sep 17, 2025
6026b37
Remove unnecessary calculation of next index in input-otp component
pushpak1300 Sep 18, 2025
970ed4e
Formatting
pushpak1300 Sep 18, 2025
ecebcf9
use when instead of assigning variable in routes file
joetannenbaum Sep 19, 2025
10944f6
formatting
joetannenbaum Sep 19, 2025
2ff6a8e
formatting
joetannenbaum Sep 19, 2025
e847251
formatting
joetannenbaum Sep 19, 2025
ac02b1b
undo typo
joetannenbaum Sep 19, 2025
d41ddc1
formatting
joetannenbaum Sep 19, 2025
eea0aae
formatting
joetannenbaum Sep 19, 2025
ead85d1
extract x data out to script tag
joetannenbaum Sep 22, 2025
d151879
reset instead of manual values
joetannenbaum Sep 22, 2025
832ef44
Revert "extract x data out to script tag"
joetannenbaum Sep 22, 2025
0c3ef7b
formatting
joetannenbaum Sep 22, 2025
4db6d9c
Update layout.blade.php
taylorotwell Sep 22, 2025
51a3350
Update input-otp.blade.php
taylorotwell Sep 22, 2025
e0e321d
Update two-factor-challenge.blade.php
taylorotwell Sep 22, 2025
f5534c7
Update AuthenticationTest.php
taylorotwell Sep 22, 2025
e387cf8
formatting
taylorotwell Sep 23, 2025
4dded7b
periods
taylorotwell Sep 23, 2025
15be9cc
some spacing
taylorotwell Sep 23, 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
3 changes: 2 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasFactory, Notifiable, TwoFactorAuthenticatable;

/**
* The attributes that are mass assignable.
Expand Down
33 changes: 33 additions & 0 deletions app/Providers/FortifyServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::twoFactorChallengeView(fn () => view('livewire.auth.two-factor-challenge'));
Fortify::confirmPasswordView(fn () => view('livewire.auth.confirm-password'));

RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}
1 change: 1 addition & 0 deletions bootstrap/providers.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

return [
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\VoltServiceProvider::class,
];
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/fortify": "^1.30",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1.1",
Expand Down
159 changes: 159 additions & 0 deletions config/fortify.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

use Laravel\Fortify\Features;

return [

/*
|--------------------------------------------------------------------------
| Fortify Guard
|--------------------------------------------------------------------------
|
| Here you may specify which authentication guard Fortify will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/

'guard' => 'web',

/*
|--------------------------------------------------------------------------
| Fortify Password Broker
|--------------------------------------------------------------------------
|
| Here you may specify which password broker Fortify can use when a user
| is resetting their password. This configured value should match one
| of your password brokers setup in your "auth" configuration file.
|
*/

'passwords' => 'users',

/*
|--------------------------------------------------------------------------
| Username / Email
|--------------------------------------------------------------------------
|
| This value defines which model attribute should be considered as your
| application's "username" field. Typically, this might be the email
| address of the users but you are free to change this value here.
|
| Out of the box, Fortify expects forgot password and reset password
| requests to have a field named 'email'. If the application uses
| another name for the field you may define it below as needed.
|
*/

'username' => 'email',

'email' => 'email',

/*
|--------------------------------------------------------------------------
| Lowercase Usernames
|--------------------------------------------------------------------------
|
| This value defines whether usernames should be lowercased before saving
| them in the database, as some database system string fields are case
| sensitive. You may disable this for your application if necessary.
|
*/

'lowercase_usernames' => true,

/*
|--------------------------------------------------------------------------
| Home Path
|--------------------------------------------------------------------------
|
| Here you may configure the path where users will get redirected during
| authentication or password reset when the operations are successful
| and the user is authenticated. You are free to change this value.
|
*/

'home' => '/dashboard',

/*
|--------------------------------------------------------------------------
| Fortify Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| Here you may specify which prefix Fortify will assign to all the routes
| that it registers with the application. If necessary, you may change
| subdomain under which all of the Fortify routes will be available.
|
*/

'prefix' => '',

'domain' => null,

/*
|--------------------------------------------------------------------------
| Fortify Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Fortify will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/

'middleware' => ['web'],

/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/

'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
],

/*
|--------------------------------------------------------------------------
| Register View Routes
|--------------------------------------------------------------------------
|
| Here you may specify if the routes returning views should be disabled as
| you may not need them when building your own application. This may be
| especially true if you're writing a custom single-page application.
|
*/

'views' => true,

/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of the Fortify features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/

'features' => [
// Features::registration(),
// Features::resetPasswords(),
// Features::emailVerification(),
// Features::updateProfileInformation(),
// Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0,
]),
],

];
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')->after('password')->nullable();
$table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable();
$table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
]);
});
}
};
138 changes: 138 additions & 0 deletions resources/views/components/input-otp.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
@props([
'digits' => 6,
'name' => 'code',
])

<div
@focus-2fa-auth-code.window="$refs.input1?.focus()"
@clear-2fa-auth-code.window="clearAll()"
class="relative"
x-data="{
totalDigits: @js($digits),
digitIndices: @js(range(1, $digits)),
init() {
$nextTick(() => {
this.$refs.input1?.focus();
});
},
getInput(index) {
return this.$refs['input' + index];
},
setValue(index, value) {
this.getInput(index).value = value;
},
getCode() {
return this.digitIndices
.map(i => this.getInput(i).value)
.join('');
},
updateHiddenField() {
this.$refs.code.value = this.getCode();
this.$refs.code.dispatchEvent(new Event('input', { bubbles: true }));
this.$refs.code.dispatchEvent(new Event('change', { bubbles: true }));
},
handleNumberKey(index, key) {
this.setValue(index, key);
if (index < this.totalDigits) {
this.getInput(index + 1).focus();
}
$nextTick(() => {
this.updateHiddenField();
});
},
handleBackspace(index) {
const currentInput = this.getInput(index);
if (currentInput.value !== '') {
currentInput.value = '';
this.updateHiddenField();
return;
}
if (index <= 1) {
return;
}
const previousInput = this.getInput(index - 1);
previousInput.value = '';
previousInput.focus();
this.updateHiddenField();
},
handleKeyDown(index, event) {
const key = event.key;
if (/^[0-9]$/.test(key)) {
event.preventDefault();
this.handleNumberKey(index, key);
return;
}
if (key === 'Backspace') {
event.preventDefault();
this.handleBackspace(index);
return;
}
},
handlePaste(event) {
event.preventDefault();
const pastedText = (event.clipboardData || window.clipboardData).getData('text');
const numericOnly = pastedText.replace(/[^0-9]/g, '');
const digitsToFill = Math.min(numericOnly.length, this.totalDigits);
this.digitIndices
.slice(0, digitsToFill)
.forEach(index => {
this.setValue(index, numericOnly[index - 1]);
});
if (numericOnly.length >= this.totalDigits) {
this.updateHiddenField();
}
},
clearAll() {
this.digitIndices.forEach(index => {
this.setValue(index, '');
});
this.$refs.code.value = '';
this.$refs.input1?.focus();
}
}"
>
<div class="flex items-center">
@for ($x = 1; $x <= $digits; $x++)
<input
x-ref="input{{ $x }}"
type="text"
inputmode="numeric"
pattern="[0-9]"
maxlength="1"
autocomplete="off"
@paste="handlePaste"
@keydown="handleKeyDown({{ $x }}, $event)"
@focus="$el.select()"
@input="$el.value = $el.value.replace(/[^0-9]/g, '').slice(0, 1)"
@class([
'flex size-10 items-center justify-center border border-zinc-300 bg-accent-foreground text-center text-sm font-medium text-accent-content transition-colors focus:border-accent focus:border-2 focus:outline-none focus:relative focus:z-10 dark:border-zinc-700 dark:focus:border-accent',
'rounded-l-md' => $x === 1,
'rounded-r-md' => $x === $digits,
'-ml-px' => $x > 1,
])
/>
@endfor
</div>

<input
{{ $attributes->except(['digits']) }}
type="hidden"
x-ref="code"
name="{{ $name }}"
minlength="{{ $digits }}"
maxlength="{{ $digits }}"
/>
</div>
Loading