Add Password Reset
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

This commit is contained in:
philipp lang 2023-09-08 00:29:37 +02:00
parent 6f4ee0b02d
commit 17eaa2e564
10 changed files with 290 additions and 1 deletions

View File

@ -0,0 +1,26 @@
<?php
namespace App\Auth;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Lang;
use Illuminate\Auth\Notifications\ResetPassword as BaseResetPassword;
class ResetPassword extends BaseResetPassword
{
/**
* Get the reset password notification mail message for the given URL.
*
* @param string $url
* @return MailMessage
*/
protected function buildMailMessage($url)
{
return (new MailMessage)
->subject(Lang::get('Passwort zurücksetzen | Adrema'))
->line(Lang::get('Du erhälst diese E-Mail, weil du eine Anfrage zum zurücksetzen deines Account-Passworts gestellt hast.'))
->action(Lang::get('Passwort zurücksetzen'), $url)
->line(Lang::get('Dieser Link wird in :count Minuten ablaufen.', ['count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]))
->line(Lang::get('Wenn du die Anfrage nicht selbst gestellt hast, ist keine weitere Aktion erforderlich.'));
}
}

View File

@ -4,6 +4,8 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails; use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Inertia\Inertia;
use Inertia\Response;
class ForgotPasswordController extends Controller class ForgotPasswordController extends Controller
{ {
@ -19,4 +21,9 @@ class ForgotPasswordController extends Controller
*/ */
use SendsPasswordResetEmails; use SendsPasswordResetEmails;
public function showLinkRequestForm(): Response
{
return Inertia::render('authentication/PasswordReset');
}
} }

View File

@ -44,7 +44,7 @@ class LoginController extends Controller
{ {
session()->put('title', 'Anmelden'); session()->put('title', 'Anmelden');
return \Inertia::render('VLogin'); return \Inertia::render('authentication/VLogin');
} }
/** /**

View File

@ -5,6 +5,9 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ResetPasswordController extends Controller class ResetPasswordController extends Controller
{ {
@ -27,4 +30,21 @@ class ResetPasswordController extends Controller
* @var string * @var string
*/ */
protected $redirectTo = RouteServiceProvider::HOME; protected $redirectTo = RouteServiceProvider::HOME;
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* @param \Illuminate\Http\Request $request
*/
public function showResetForm(Request $request): Response
{
$token = $request->route()->parameter('token');
return Inertia::render('authentication/PasswordResetConfirm', [
'token' => $token,
'email' => $request->email,
]);
}
} }

View File

@ -2,12 +2,24 @@
namespace App; namespace App;
use App\Auth\ResetPassword;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable class User extends Authenticatable
{ {
use HasFactory; use HasFactory;
use Notifiable;
public $guarded = []; public $guarded = [];
/**
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPassword($token));
}
} }

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('password_resets');
}
};

View File

@ -0,0 +1,44 @@
<template>
<page-full-layout banner>
<template #heading>
<page-full-heading-banner>Passwort vergessen</page-full-heading-banner>
</template>
<form @submit.prevent="submit">
<div class="grid gap-5">
<span class="text-gray-500 text-sm"
>Hier kannst du dein Passwort zurücksetzen.<br />
Gebe dafür die E-Mail-Adresse deines Benutzerkontos ein.<br />
Anschließend bekommst du eine E-Mail<br />
mit weiteren Anweisungen.</span
>
<f-text id="email" v-model="values.email" name="email" label="E-Mail-Adresse"></f-text>
<button type="submit" class="btn btn-primary">Passwort zurücksetzen</button>
<div class="flex justify-center">
<button type="button" class="text-gray-500 text-sm hover:text-gray-300" @click.prevent="$inertia.visit('/login')">Zurück zum Login</button>
</div>
</div>
</form>
</page-full-layout>
</template>
<script>
import FullLayout from '../../layouts/FullLayout.vue';
export default {
layout: FullLayout,
data: function () {
return {
values: {
email: '',
},
};
},
methods: {
async submit() {
await this.axios.post('/password/email', this.values);
this.$success('Du hast weitere Instruktionen per E-Mail erhalten.');
},
},
};
</script>

View File

@ -0,0 +1,62 @@
<template>
<page-full-layout banner>
<template #heading>
<page-full-heading-banner>Passwort vergessen</page-full-heading-banner>
</template>
<form @submit.prevent="submit">
<div class="grid gap-5">
<span class="text-gray-500 text-sm"
>Hier kannst du dein Passwort zurücksetzen.<br />
Gebe dafür ein neues Passwort ein.<br />
Merke oder notiere dir dieses Passwort, bevor du das Formular absendest.<br />
Danach wirst du zum Dashboard weitergeleitet.</span
>
<f-text id="password" v-model="values.password" type="password" name="password" label="Neues Passwort"></f-text>
<f-text id="password_confirmation" v-model="values.password_confirmation" type="password" name="password_confirmation" label="Neues Passwort widerholen"></f-text>
<button type="submit" class="btn btn-primary">Passwort zurücksetzen</button>
<div class="flex justify-center">
<button type="button" class="text-gray-500 text-sm hover:text-gray-300" @click.prevent="$inertia.visit('/login')">Zurück zum Login</button>
</div>
</div>
</form>
</page-full-layout>
</template>
<script>
import FullLayout from '../../layouts/FullLayout.vue';
export default {
layout: FullLayout,
props: {
token: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
},
data: function () {
return {
values: {
password: '',
password_confirmation: '',
},
};
},
methods: {
async submit() {
await this.axios.post('/password/reset', {
...this.values,
email: this.email,
token: this.token,
});
this.$success('Dein Passwort wurde zurückgesetzt.');
this.$inertia.visit('/');
},
},
};
</script>

View File

@ -0,0 +1,40 @@
<template>
<page-full-layout banner>
<template #heading>
<page-full-heading-banner>Login</page-full-heading-banner>
</template>
<form @submit.prevent="submit">
<div class="grid gap-5">
<f-text id="email" v-model="values.email" name="email" label="E-Mail-Adresse"></f-text>
<f-text id="password" v-model="values.password" name="password" type="password" label="Passwort"></f-text>
<button type="submit" class="btn btn-primary">Login</button>
<div class="flex justify-center">
<button type="button" class="text-gray-500 text-sm hover:text-gray-300"
@click.prevent="$inertia.visit('/password/reset')">Passwort vergessen?</button>
</div>
</div>
</form>
</page-full-layout>
</template>
<script>
import FullLayout from '../../layouts/FullLayout.vue';
export default {
layout: FullLayout,
data: function () {
return {
values: {
email: '',
password: '',
},
};
},
methods: {
submit() {
this.$inertia.post('/login', this.values);
},
},
};
</script>

View File

@ -0,0 +1,46 @@
<?php
namespace Tests\Feature\Authentication;
use App\Auth\ResetPassword;
use App\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class ForgotPasswordTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsResetForm(): void
{
$this->withoutExceptionHandling();
$response = $this->get('/password/reset');
$this->assertComponent('authentication/PasswordReset', $response);
}
public function testItRequiresAnEmailAddress(): void
{
$this->postJson('/password/email')->assertJsonValidationErrors(['email' => 'E-Mail Adresse ist erforderlich.']);
}
public function testItNeedsAnActiveUser(): void
{
$this->postJson('/password/email', [
'email' => 'test@aa.de',
])->assertJsonValidationErrors(['email' => 'Es konnte leider kein Nutzer mit dieser E-Mail-Adresse gefunden werden.']);
}
public function testItSendsPasswordResetLink(): void
{
Notification::fake();
$user = User::factory()->create(['email' => 'test@aa.de']);
$this->postJson('/password/email', [
'email' => 'test@aa.de',
])->assertOk();
Notification::assertSentTo($user, ResetPassword::class);
}
}