環境
- Laravel 8.6
- PHP 8.0
- Bootstrap 5.1
11.1 AccountActivationsリソース
11.1.1 AccountActivationsコントローラ
/routes/web.php
Route::get('account_activations/{token}/edit', [AccountActivationsController::class, 'edit'])->name('activation');
11.1.2 AccountActivationのデータモデル
/database/migrations/[timestamp]_add_activation_to_users.php
class AddActivationToUsers extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('activation_digest')->nullable();
$table->boolean('activated')->default(false);
$table->dateTime('activated_at')->nullable();
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('activation_digest');
$table->dropColumn('activated');
$table->dropColumn('activated_at');
});
}
}
/app/Http/Controllers/UsersController.php
public function store(Request $request)
{
$request->email = Str::lower($request->email);
$request->validate([
"name" => "required|max:50",
"email" => "required|max:255|email|unique:users",
"password" => "required|confirmed|min:6",
"password_confirmation" => "required|min:6",
]);
$user = new User;
$user->name = $request->name;
$user->email = $request->email;
$user->password = bcrypt($request->password);
$activation_token = Str::random(22);
$user->activation_digest = bcrypt($activation_token);
$user->save();
Auth::login($user);
session()->flash("message", ['success' => 'Welcome to the Sample App!']);
return redirect()->route("users.show", $user);
}
/database/factories/UserFactory.php
$factory->define(User::class, function (Faker $faker) {
return [
'name' => $this->faker->name(),
'email' => "example-{$this->faker->unique()->randomNumber}@railstutorial.org",
'password' => bcrypt("password"),
'activated' => true,
'activated_at' => Carbon::now(),
];
});
/database/seeds/DatabaseSeeder.php
DB::table('users')->insert([
"name" => "Example User",
"email" => "example@railstutorial.org",
"password" => bcrypt("foobar"),
"admin" => true,
"activated" => true,
"activated_at" => Carbon::now(),
]);
/database/seeds/test/TestSeeder.php
User::create(["name" => "Michael Example", "email" => "michael@example.com", "password" => bcrypt("password"), "admin" => true, "activated" => true, "activated_at" => Carbon::now()]);
User::create(["name" => "Sterling Archer", "email" => "duchess@example.gov", "password" => bcrypt("password"), "activated" => true, "activated_at" => Carbon::now()]);
User::create(["name" => "Lana Kane", "email" => "hands@example.gov", "password" => bcrypt("password"), "activated" => true, "activated_at" => Carbon::now()]);
User::create(["name" => "Malory Archer", "email" => "boss@example.gov", "password" => bcrypt("password"), "activated" => true, "activated_at" => Carbon::now()]);
11.2 アカウント有効化のメール送信
11.2.1 送信メールのテンプレート
sail artisan make:mail AccountActivation
sail artisan make:mail PasswordReset
/resources/views/emails/account_activation.blade.php
<h1>Sample App</h1>
<p>Hi {{ $user->name }},</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
{{ link_to_route("activation", "Activate", ["token" => $activation_token, "email" => $user->email]) }}
/app/Mail/AccountActivation.php
class AccountActivation extends Mailable
{
use Queueable, SerializesModels;
public $user;
public $activation_token;
public function __construct($user, $activation_token)
{
$this->user = $user;
$this->activation_token = $activation_token;
}
public function build()
{
return $this->from("noreply@example.com")
->subject("Account activation")
->view('emails.account_activation');
}
}
/app/Http/Controllers/UsersController.php
Mail::to($user)->send(new AccountActivation($user, $activation_token));
11.2.2 送信メールのプレビュー
mailHogを使用する
https://readouble.com/laravel/8.x/ja/sail.html
11.2.3 送信メールのテスト
/tests/Unit/MailerTest.php
protected function setUp(): void
{
parent::setUp();
Artisan::call('migrate:fresh');
$this->seed('TestSeeder');
}
public function testAccountActivation()
{
Mail::fake();
$user = User::find(1);
$activation_token = Str::random(22);
Mail::to($user)->send(new AccountActivation($user, $activation_token));
Mail::assertSent(AccountActivation::class, function ($mail) use ($user, $activation_token) {
$mail->build();
$this->assertEquals("Account activation", $mail->subject);
$this->assertTrue($mail->hasTo($user->email));
$this->assertTrue($mail->hasFrom("noreply@example.com"));
$this->assertEquals($user->name, $mail->user->name);
$this->assertEquals($activation_token, $mail->activation_token);
$this->assertEquals($user->email, $mail->user->email);
return true;
});
}
11.2.4 ユーザーのcreateアクションを更新
/app/Http/Controllers/UsersController.php
$user->save();
Mail::to($user)->send(new AccountActivation($user, $activation_token));
session()->flash('message', ['info' => 'Please check your email to activate your account.']);
return redirect("/");
/tests/Feature/UsersSignupTest.php
// $response->assertViewIs("users.show");
// $this->assertTrue(Auth::check());
11.3 アカウントを有効化する
11.3.1 authenticated?メソッドの抽象化
11.3.2 editアクションで有効化
/app/Http/Controllers/AccountActivationsController.php
class AccountActivationsController extends Controller
{
public function edit(Request $request, $token)
{
$user = User::where('email', $request->email)->first();
if ($user && !$user->activated && Hash::check($token, $user->activation_digest)) {
$user->activated = true;
$user->activated_at = Carbon::now();
$user->save();
Auth::login($user);
session()->flash('message', ['success' => 'Account activated!']);
return redirect()->route("users.show", $user->id);
} else {
session()->flash('message', ['danger' => 'Invalid activation link']);
return redirect("/");
}
}
}
/app/Http/Controllers/SessionController.php
public function store(Request $request)
{
$user = User::where('email', $request->email)->first();
if ($user && Hash::check($request->password, $user->password)) {
if ($user->activated === true) {
Auth::login($user, $request->remember_me === "1");
return redirect()->intended("/users/" . Auth::id());
} else {
session()->flash('message', ['warning' => 'Account not activated. Check your email for the activation link.']);
return redirect("/");
}
} else {
session()->flash('message', ['danger' => 'Invalid email/password combination']);
return back()->withInput();
}
}
11.3.3 有効化のテストとリファクタリング
/tests/Feature/UsersSignupTest.php
class UsersSignupTest extends TestCase
{
use RefreshDatabase;
public function testInvalidSignup()
{
$this->get("signup");
$count = User::all()->count();
$response = $this->followingRedirects()
->post(route("signup"), [
"name" => "",
"email" => "user@invalid",
"password" => "foo",
"password_confirmation" => "bar"
]);
$this->assertSame($count, User::all()->count());
$response->assertViewIs("users.create");
$dom = $this->dom($response->content());
$this->assertSame(1, $dom->filter('div#error_explanation')->count());
$this->assertSame(1, $dom->filter('div.alert-danger')->count());
}
public function testValidSignupWithAccountActivation()
{
Mail::fake();
$this->get(route('signup'));
$count = User::all()->count();
$response = $this->followingRedirects()
->post(route('signup'), [
'name' => "Example User",
'email' => "user@example.com",
'password' => "password",
'password_confirmation' => "password",
]);
$this->assertSame($count + 1, User::all()->count());
Mail::assertSent(AccountActivation::class, 1);
$user = User::where("email", "user@example.com")->first();
$activation_token = Str::random(22);
$user->update(["activation_digest" => bcrypt($activation_token)]);
$this->assertFalse($user->activated);
$this->post("login", ["email" => $user->email, "password" => "password"]);
$this->assertFalse(Auth::check());
$this->get(route("activation", ["token" => "incalid token", "email" => $user->email]));
$this->assertFalse(Auth::check());
$this->get(route("activation", ["token" => $activation_token, "email" => "wrong"]));
$this->assertFalse(Auth::check());
$response = $this->get(route("activation", ["token" => $activation_token, "email" => $user->email]));
$this->assertTrue(User::find($user->id)->activated);
$response->assertRedirect(route("users.show", $user->id));
$this->assertTrue(Auth::check());
}
}
/app/Http/Models/User.php
protected $casts = [
'email_verified_at' => 'datetime',
'admin' => 'boolean',
'activated' => 'boolean',
];
public function activate()
{
$this->activated = true;
$this->activated_at = Carbon::now();
$this->save();
}
public function sendActivateEmail($token)
{
Mail::to($this)->send(new AccountActivation($this, $token));
}
課題
/app/Http/Controllers/UsersController.php
public function index()
{
$users = User::where("activated", true)->paginate(30);
return view('users.index')->with('users', $users);
}
...
public function show(User $user)
{
if ($user->activated) {
return view('users.show')->with('user', $user);
} else {
return redirect("/");
}
}
11.4 本番環境でのメール送信
sendgridを使用
heroku addons:create sendgrid:starter
heroku config:set MAIL_HOST=smtp.sendgrid.net
heroku config:set MAIL_USERNAME={heroku config:get SENDGRID_USERNAME}
heroku config:set MAIL_PASSWORD={heroku config:get SENDGRID_PASSWORD}
11.5 最後に
https://windii.jp/backend/laravel/api-register
https://www.ritolab.com/entry/38
https://sendgrid.com/docs/for-developers/sending-email/laravel/