feat: forget-password & reset-password added

This commit is contained in:
2025-09-13 13:07:18 +03:30
parent 00edec179a
commit b820dd372a
8 changed files with 371 additions and 73 deletions
+50
View File
@@ -18,9 +18,59 @@
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
class UserController extends Controller class UserController extends Controller
{ {
public function requestPasswordReset(Request $request)
{
$request->validate([
'email' => 'required|email',
]);
$status = Password::sendResetLink(
$request->only('email')
);
if ($status === Password::RESET_LINK_SENT) {
return response()->json(['message' => __($status)]);
}
return response()->json(['message' => __($status)], 400);
}
public function showResetForm($token)
{
return view('auth.reset-password', ['token' => $token]);
}
public function resetPassword(Request $request)
{
$request->validate([
'email' => 'required|email',
'token' => 'required|string',
'password' => 'required|string|confirmed|min:6',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => $password,
])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
if ($status === Password::PASSWORD_RESET) {
return response()->json(['message' => __($status)]);
}
return response()->json(['message' => __($status)], 400);
}
public function login(Request $request) public function login(Request $request)
{ {
$data = $request->validate([ $data = $request->validate([
+1 -2
View File
@@ -11,7 +11,6 @@
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
"laravel/socialite": "^5.6", "laravel/socialite": "^5.6",
"laravel/telescope": "^5.11",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"masbug/flysystem-google-drive-ext": "^2.2", "masbug/flysystem-google-drive-ext": "^2.2",
"predis/predis": "^2.1", "predis/predis": "^2.1",
@@ -24,10 +23,10 @@
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"laravel/pint": "^1.0", "laravel/pint": "^1.0",
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"laravel/telescope": "^5.11",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"phpunit/phpunit": "^10.1", "phpunit/phpunit": "^10.1",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
Generated
+70 -70
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9014e7db7181477837583be158dad2c3", "content-hash": "93b523c73b637a7401d21d02b2b72234",
"packages": [ "packages": [
{ {
"name": "beste/clock", "name": "beste/clock",
@@ -2850,75 +2850,6 @@
}, },
"time": "2025-07-23T14:16:08+00:00" "time": "2025-07-23T14:16:08+00:00"
}, },
{
"name": "laravel/telescope",
"version": "v5.11.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/telescope.git",
"reference": "7684604e104e7755b70dcacfeee06888e2470689"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/telescope/zipball/7684604e104e7755b70dcacfeee06888e2470689",
"reference": "7684604e104e7755b70dcacfeee06888e2470689",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0",
"php": "^8.0",
"symfony/console": "^5.3|^6.0|^7.0",
"symfony/var-dumper": "^5.0|^6.0|^7.0"
},
"require-dev": {
"ext-gd": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"laravel/octane": "^1.4|^2.0|dev-develop",
"orchestra/testbench": "^6.40|^7.37|^8.17|^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.0|^10.5|^11.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Telescope\\TelescopeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Telescope\\": "src/",
"Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Mohamed Said",
"email": "mohamed@laravel.com"
}
],
"description": "An elegant debug assistant for the Laravel framework.",
"keywords": [
"debugging",
"laravel",
"monitoring"
],
"support": {
"issues": "https://github.com/laravel/telescope/issues",
"source": "https://github.com/laravel/telescope/tree/v5.11.3"
},
"time": "2025-08-21T14:25:40+00:00"
},
{ {
"name": "laravel/tinker", "name": "laravel/tinker",
"version": "v2.10.1", "version": "v2.10.1",
@@ -9608,6 +9539,75 @@
}, },
"time": "2025-08-25T19:28:31+00:00" "time": "2025-08-25T19:28:31+00:00"
}, },
{
"name": "laravel/telescope",
"version": "v5.11.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/telescope.git",
"reference": "7684604e104e7755b70dcacfeee06888e2470689"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/telescope/zipball/7684604e104e7755b70dcacfeee06888e2470689",
"reference": "7684604e104e7755b70dcacfeee06888e2470689",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0",
"php": "^8.0",
"symfony/console": "^5.3|^6.0|^7.0",
"symfony/var-dumper": "^5.0|^6.0|^7.0"
},
"require-dev": {
"ext-gd": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"laravel/octane": "^1.4|^2.0|dev-develop",
"orchestra/testbench": "^6.40|^7.37|^8.17|^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.0|^10.5|^11.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Telescope\\TelescopeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Telescope\\": "src/",
"Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Mohamed Said",
"email": "mohamed@laravel.com"
}
],
"description": "An elegant debug assistant for the Laravel framework.",
"keywords": [
"debugging",
"laravel",
"monitoring"
],
"support": {
"issues": "https://github.com/laravel/telescope/issues",
"source": "https://github.com/laravel/telescope/tree/v5.11.3"
},
"time": "2025-08-21T14:25:40+00:00"
},
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
"version": "1.6.12", "version": "1.6.12",
@@ -0,0 +1,70 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Get the migration connection name.
*/
public function getConnection(): ?string
{
return config('telescope.storage.database.connection');
}
/**
* Run the migrations.
*/
public function up(): void
{
$schema = Schema::connection($this->getConnection());
$schema->create('telescope_entries', function (Blueprint $table) {
$table->bigIncrements('sequence');
$table->uuid('uuid');
$table->uuid('batch_id');
$table->string('family_hash')->nullable();
$table->boolean('should_display_on_index')->default(true);
$table->string('type', 20);
$table->longText('content');
$table->dateTime('created_at')->nullable();
$table->unique('uuid');
$table->index('batch_id');
$table->index('family_hash');
$table->index('created_at');
$table->index(['type', 'should_display_on_index']);
});
$schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->uuid('entry_uuid');
$table->string('tag');
$table->primary(['entry_uuid', 'tag']);
$table->index('tag');
$table->foreign('entry_uuid')
->references('uuid')
->on('telescope_entries')
->onDelete('cascade');
});
$schema->create('telescope_monitoring', function (Blueprint $table) {
$table->string('tag')->primary();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$schema = Schema::connection($this->getConnection());
$schema->dropIfExists('telescope_entries_tags');
$schema->dropIfExists('telescope_entries');
$schema->dropIfExists('telescope_monitoring');
}
};
@@ -0,0 +1,28 @@
<?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::create('password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('password_resets');
}
};
@@ -0,0 +1,28 @@
<?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->rememberToken()->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('remember_token');
});
}
};
@@ -0,0 +1,117 @@
<!doctype html>
<html lang="fa">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>بازیابی رمز عبور</title>
<style>
body {
font-family: 'Tahoma', 'IRANYekan', sans-serif;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
background: #f5f7fb;
direction: rtl;
}
.card {
background: #fff;
padding: 28px;
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0,0,0,.06);
width: 380px;
}
input {
width: 100%;
padding: 10px;
margin: 8px 0;
border: 1px solid #ddd;
border-radius: 6px;
}
button {
width: 100%;
padding: 10px;
border: 0;
border-radius: 6px;
background: #2563eb;
color: #fff;
font-weight: 600;
}
.msg {
margin-top: 12px;
padding: 8px;
border-radius: 6px;
word-break: break-word;
}
</style>
</head>
<body>
<div class="card">
<h2 style="margin:0 0 8px">بازیابی رمز عبور</h2>
<p style="margin:0 0 12px;font-size:13px;color:#555">
لطفاً رمز عبور جدید خود را وارد کنید
</p>
<form id="resetForm" autocomplete="off">
<input id="email" name="email" type="email" placeholder="ایمیل (خودکار پر شده)" required />
<input id="password" name="password" type="password" placeholder="رمز عبور جدید" required />
<input id="password_confirmation" name="password_confirmation" type="password" placeholder="تأیید رمز عبور" required />
<input id="token" name="token" type="hidden" value="{{ $token }}" />
<button type="submit">بازنشانی رمز عبور</button>
</form>
<div id="result" class="msg" style="display:none"></div>
</div>
<script>
document.getElementById('resetForm').addEventListener('submit', async function (e) {
e.preventDefault();
const resultEl = document.getElementById('result');
resultEl.style.display = 'block';
resultEl.style.background = '#fff7ed';
resultEl.style.color = '#7c2d12';
resultEl.textContent = 'در حال پردازش...';
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value;
const password_confirmation = document.getElementById('password_confirmation').value;
const token = document.getElementById('token').value;
try {
const res = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, token, password, password_confirmation })
});
const json = await res.json();
if (res.ok) {
resultEl.style.background = '#ecfdf5';
resultEl.style.color = '#064e3b';
resultEl.textContent = 'رمز عبور با موفقیت بازنشانی شد. اکنون می‌توانید وارد شوید.';
} else {
resultEl.style.background = '#fff1f2';
resultEl.style.color = '#7f1d1d';
if (json.errors) {
const first = Object.values(json.errors)[0];
resultEl.textContent = Array.isArray(first) ? first[0] : first;
} else {
resultEl.textContent = json.message || JSON.stringify(json);
}
}
} catch (err) {
resultEl.style.background = '#fff1f2';
resultEl.style.color = '#7f1d1d';
resultEl.textContent = 'خطای شبکه.';
console.error(err);
}
});
const qs = new URLSearchParams(window.location.search);
const emailFromQuery = qs.get('email');
if (emailFromQuery) document.getElementById('email').value = emailFromQuery;
</script>
</body>
</html>
+6
View File
@@ -23,7 +23,13 @@
Route::post('{id}', 'update'); Route::post('{id}', 'update');
Route::get('', 'index'); Route::get('', 'index');
}); });
Route::prefix('auth')->controller(UserController::class)->group(function () {
Route::post('forgot-password', 'requestPasswordReset'); // send reset link
Route::post('reset-password', 'resetPassword'); // actually reset
});
Route::get('/reset-password/{token}', [UserController::class, 'showResetForm'])
->name('password.reset');
Route::prefix('auth')->controller(UserController::class)->group(function () { Route::prefix('auth')->controller(UserController::class)->group(function () {
Route::middleware(['throttle:3,1', 'auto-ban'])->post('login-otp', 'loginOTP'); Route::middleware(['throttle:3,1', 'auto-ban'])->post('login-otp', 'loginOTP');
Route::post('check-otp', 'checkOTP'); Route::post('check-otp', 'checkOTP');