0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実務未経験エンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(バックエンド実装編⑮)~ソーシャルログイン機能作成~

Posted at

実務未経験エンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その21)

0. 初めに

こんにちは!
研修中エンジニアの僕がWebアプリケーションを作成する過程をお届けしているシリーズです。

ついに、バックエンド編も最終回となりました!
とてつもない長さになってしまいましたね...

結論として、個人で開発するには、設計の段階で機能を盛りすぎた気がします。(苦笑)

しかし、一度始めたことなので、形になるものをどうにか作り上げたいなと考えております!
もう少々お付き合いくださるとうれしいです!!

今日作るのは、ソーシャルログインと呼ばれる機能です。
頑張っていきましょう!

ソーシャルログインとは

皆さんは、これまで何かしらのWebサービスを使っていて、新規登録するときなどに、「Googleでログイン」というボタンが表示されたことはないでしょうか?
他にも、「LINEでログイン」とか「Xでログイン」とか、他のSNSを利用したログイン方法を一度は目にしたことがあると思います。

これらは、ソーシャルログインと呼ばれており、このように外部のサービスやアプリケーションを通じてログインすることができます。

今日は、Googeleのアカウントを持っている人なら、今回のシリーズで作っている研究室レビューアプリにメールアドレスとパスワードの登録がなくても新規登録できるようにしたいと思います!

ソーシャルログインの仕組みやメリット・デメリットについては以下の記事が分かりやすいかなと思いますので、事前に目を通しておくとよいかもしれません!
最近よく見る「Googleでログイン」 の仕組みやメリットを解説

また、僕自身がGoogleでのソーシャルログインを実装するのが初めてなため、基本的には以下の記事を参考にして進めていきたいと思います。
LaravelでサクっとはじめるGoogleログイン

1. ブランチ運用

前回の作業が終わっていたらいつも通り、コミット・プッシュして、リモートのdevelopに対してマージしておきましょう。

それが完了していたら、ローカルのdevelopにプルして、そこから新しいブランチを切って今回の作業をしてきましょう。
ブランチ名は、feature/19-social-loginにしたいと思います。

いつも通り、作業が完了したらコミット・プッシュ、リモートのdevelopブランチへのマージを忘れずに行いましょう。

2. Googel Cloud設定

「Googleでログイン」を実現するには、当然Googleの機能を利用することが必要です。
そのために利用するのが、Googel Cloudというクラウドサービスです。

「クラウドサービスって何?」って感じの方もいらっしゃると思いますが、詳しい説明は今後のデプロイ編でお話しできれば良いかなと思っているので、今日は説明を割愛します。

要するに、Googelが提供してくれているWeb上で利用できる便利な機能がたくさんあるサービスです。

無料枠利用を開始する

Googleのアカウントがある方ならすぐに無料枠で利用を開始できます。
今回のソーシャルログインを実装するためのアカウントでGoogle Cloudにログインしましょう。
ただし、住所とクレジットカード情報の入力が必要です。
https://cloud.google.com/free?hl=ja

新規プロジェクトを作成する

画面上部のナビゲーションの「プロジェクトを選択」をクリックします(デフォルトだとMy First Projectというプロジェクトが選択されている状態だと思います)。
image.png

すると、モーダル画面が表示されるので、右上の「新しいプロジェクト」をクリックします。
image.png

適当な名前で、新しいプロジェクトを作成しましょう。
image.png

新しく作成したプロジェクトに切り替えましょう(プロジェクトが作成されるまで少し時間がかかるかもしれません)。
image.png

※参考記事にもある通り、これの通りに従えばできます。
https://cloud.google.com/resource-manager/docs/creating-managing-projects?hl=ja#creating_a_project

OAuth同意画面を設定する

OAuthは、他のサービスのアカウントを用いた認証を実現する仕組みのことで、今回のソーシャルログイン機能を作成するために利用します!
詳しい説明を聞きたい方は、以下の記事なんかが良いかもです。
一番わかりやすいらしいです。
一番分かりやすい OAuth の説明

Google CloudもOAuthのサービスを提供しており、これによって「Googleでログイン」が利用できるというわけです。
さらに、後から紹介しますが、Laravelの便利ライブラリのLaravel SocialiteがLaravel側でOAuthの仕組みを通じたGoogleとのユーザーデータのやり取りを簡単に行えるようにしてくれます。

Google CloudでOAuthの機能を利用するためには、OAuthクライアントID(説明は後述します)というものを発行する必要があります。
さらに、そのためには、OAuth同意というものが必要になります。

実際の画面で設定していく中で、説明をしていきます。

また、以降の説明は、以下を参考にしてくださってもかまいません。
https://support.google.com/workspacemigrate/answer/9222992?hl=ja

まず、先ほどの作成したプロジェクトが選択された状態の画面で中央にある「API」メニューカードの一番下にある「APIの概要に移動」をクリックします。
image.png

すると、「APIとサービス」という画面が開くので、左側のサイドバーにある「OAuth同意画面」をクリックします。
image.png

初めてだとこんな感じの画面になると思いますので、「開始」を押してみます。
image.png

必要項目を入力して、「作成」をクリックしましょう。
image.png

※「②対象」では、「外部」を選択してください。
スクリーンショット 2025-10-12 161519.png

OAuthクライアントIDを発行する

先ほどまでの手順が完了すると以下のような画面が表示されると思いますので、「OAuthクライアントを作成」をクリックします。
image.png

「アプリケーションの種類」は「ウェブアプリケーション」を選択します。
名前は任意で大丈夫です。
image.png

「承認済みのJavaScript生成元」は空欄でOKです。
「承認済みのリダイレクトURI」に以下を入力して、「作成」をクリック。
http://localhost/auth/google/callback
image.png

作成すると表示される以下の二つはこの後すぐに使うので、まだページを閉じないでおきましょう。

  • クライアント ID
  • クライアント シークレット

これらについて説明します。
クライアント IDは、アプリケーションを識別するためのものです。
つまり、先ほど紹介した一番分かりやすい OAuth の説明における「クライアントアプリ」を識別するために発行する必要があります。
そして、このクライアントアプリは今回で言うと、今まさに僕が作成している研究室レビューアプリです。

リソースサーバーである、Googleがどのアプリからユーザー情報を提供を求められているのかを区別することができます。

また、クライアント シークレットは、これに対応するパスワードだと思ってください。
これにより、本当に僕のアプリかどうかを判断することができます。

.envを設定する

続いて、Laravelのソースコードに戻って、設定ファイルに今の二つをコピペしましょう。
クライアントIDをGOOGLE_CLIENT_IDに、クライアントシークレットをGOOGLE_CLIENT_SECRETを設定しましょう。

\project-root\src.env
GOOGLE_CLIENT_ID=あなたのクライアントIDをここに
GOOGLE_CLIENT_SECRET=あなたのクライアントシークレットをここに
GOOGLE_REDIRECT_URL=http://localhost/auth/google/callback

srcディレクトリの一つ上にDokcerで環境構築した時のプロジェクト全体の.envファイルがそこにもあり、それと間違いやすいので注意ですね。(; ・`д・´)

\project-root\src\config\services.php
    // 追加
    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('GOOGLE_REDIRECT_URL'),
    ],

ソーシャルログインを実装する

Laravel Socialite導入

Laravel SocialiteはOAuthを簡単に実現することができる便利なライブラリです。

Laravelは、一般的なフォームベースの認証に加えて、Laravel Socialite(ソーシャライト:名士)を使用したOAuthプロバイダで認証するためのシンプルで便利な方法も提供します。Socialiteは現在、Facebook、X、LinkedIn、Google、GitHub、GitLab、Bitbucket、Slackでの認証をサポートしています。

ドキュメントの説明通り、今回実装しようとしてるGoogleのソーシャルログインにも対応しています!

以下のコマンドで、インストールしましょう。

実行コマンド

/var/www
$ composer require laravel/socialite

マイグレーション修正・実行

新しく、カラムを追加するマイグレーションファイルを作ってもよかったのですが、よく考えたらまだ開発段階なので初期のテーブルをそのまま修正しちゃった方がシンプルかなと思いました。

ってか今までも、それで統一するべきでしたよね...ばらばらですみません。
up()メソッドの中身を修正します。

\project-root\src\database\migrations\0001_01_01_000000_create_users_table.php
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable(); // 修正: ソーシャルログイン用にnullableに変更
            // 追加: Googleログイン用のカラム
            $table->string('google_id')->nullable()->unique();
            
            $table->rememberToken();
            $table->timestamps();
        });

実行コマンド

/var/www
$ php artisan migrate:fresh --seed

実行すると何やら、エラー。
image.png

おそらく、前回通知機能を作成した際に追加した新しいカラムのcreated_byに対するシーダーを用意していないからだと思います。

以下のようにして、あらかじめ管理者を作成しておくように修正します。

\project-root\src\database\seeders\DatabaseSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Lab; // 追加
use App\Models\Review; // 追加
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // 追加: 管理者を先に作成
        $email = 'admin@example.com';

        $admin = User::updateOrCreate(
            ['email' => $email],
            [
                'name' => 'シード管理者',
                'password' => Hash::make('password'),
                'is_admin' => true,
            ]
        );

        // 大学・学部・研究室のSeederクラスを呼び出す
        $this->call([
            UniversitySeeder::class,
            FacultySeeder::class,
            LabSeeder::class,
        ]);

        // ユーザー30人分を作成、さらにリレーションを付与
        User::factory()->count(30)->create()->each(function ($user) {
            // 大学・学部・研究室のリレーションを付与
            $user->universities()->attach(rand(1, 3)); // university_id: 1~3
            $user->faculties()->attach(rand(1, 4)); // faculty_id: 1~4
            $user->labs()->attach(rand(1, 5)); // lab_id: 1~5
        });

        // レビューを50件作成
        // ただし、重複を回避しながら作成
        $users = User::all();
        $labs = Lab::all();
        $createdCombinations = [];
        $targetCount = 50;

        while (count($createdCombinations) < $targetCount) {
            $userId = $users->random()->id;
            $labId = $labs->random()->id;
            $combination = "{$userId}-{$labId}";

            if (!in_array($combination, $createdCombinations)) {
                Review::factory()->create([
                    'user_id' => $userId,
                    'lab_id' => $labId,
                ]);
                $createdCombinations[] = $combination;
            }
        }
    }
}

updateOrCreate()は、第一引数に検索条件の配列、第二引数に更新したいデータの配列を渡すことで、モデルの検索と更新を一度に行える便利なメソッドです。
詳しくは、こちらの記事が分かりやすいでしょう。
LaravelのupdateOrCreateを完全理解!5つの実践的なユースケースと性能最適化テクニック

さらに、大学・学部・研究室のそれぞれのシーダーに作成者の情報を入れるように修正します。
先ほど設定した管理者を探してcreated_byに設定するようにします。

\project-root\src\database\seeders\UniversitySeeder.php
<?php

namespace Database\Seeders;

use App\Models\University;
use App\Models\User; // 追加
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class UniversitySeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // 追加: 作成者を管理者に設定
        $adminId = User::where('email', 'admin@example.com')->value('id')
            ?? User::first()->id; // 念のため、管理者がいなければ最初のユーザーを使用
        // 大学を適当に3つ作成
        University::create([
            'name' => '荒木大学',
            'created_by' => $adminId, // 追加
        ]);

        University::create([
            'name' => '荒木県立大学',
            'created_by' => $adminId, // 追加
        ]);

        University::create([
            'name' => '荒木市立大学',
            'created_by' => $adminId, // 追加
        ]);
    }
}

\project-root\src\database\seeders\FacultySeeder.php
<?php

namespace Database\Seeders;

use App\Models\Faculty;
use App\Models\User; // 追加
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class FacultySeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // 追加: 作成者を管理者に設定
        $adminId = User::where('email', 'admin@example.com')->value('id')
            ?? User::first()->id;

        // 学部を適当に4つ作成
        Faculty::create([
            'university_id' => 1,
            'name' => '教養学部',
            'created_by' => $adminId, // 追加
        ]);

        Faculty::create([
            'university_id' => 1,
            'name' => '経済学部',
            'created_by' => $adminId, // 追加
        ]);

        Faculty::create([
            'university_id' => 1,
            'name' => '理学部',
            'created_by' => $adminId, // 追加
        ]);

        Faculty::create([
            'university_id' => 1,
            'name' => '工学部',
            'created_by' => $adminId, // 追加
        ]);
    }
}

\project-root\src\database\seeders\LabSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Lab;
use App\Models\User; // 追加
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class LabSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // 追加: 作成者を管理者に設定
        $adminId = User::where('email', 'admin@example.com')->value('id')
            ?? User::first()->id;

        // 研究室を適当に5つ作成
        Lab::create([
            'faculty_id' => 4,
            'name' => '機械工学科 材料工学研究室',
            'description' => '材料の力学的性質を調べる研究室です。',
            'url' => 'https://example.com/lab1',
            'professor_url' => 'https://example.com/professor1',
            'gender_ratio_male' => 6,
            'gender_ratio_female' => 4,
            'created_by' => $adminId, // 追加
        ]);

        Lab::create([
            'faculty_id' => 4,
            'name' => '情報工学科',
            'description' => '情報処理技術を学ぶ研究室です。',
            'url' => 'https://example.com/lab2',
            'professor_url' => 'https://example.com/professor2',
            'gender_ratio_male' => 5,
            'gender_ratio_female' => 5,
            'created_by' => $adminId, // 追加
        ]);

        Lab::create([
            'faculty_id' => 4,
            'name' => '応用化学科 荒木研究室',
            'description' => '化学の応用を学ぶ研究室です。',
            'url' => 'https://example.com/lab3',
            'professor_url' => 'https://example.com/professor3',
            'gender_ratio_male' => 7,
            'gender_ratio_female' => 3,
            'created_by' => $adminId, // 追加
        ]);

        Lab::create([
            'faculty_id' => 4,
            'name' => '情報工学科',
            'description' => '情報処理技術を学ぶ研究室です。',
            'url' => 'https://example.com/lab4',
            'professor_url' => 'https://example.com/professor4',
            'gender_ratio_male' => 5,
            'gender_ratio_female' => 5,
            'created_by' => $adminId, // 追加
        ]);

        Lab::create([
            'faculty_id' => 4,
            'name' => '機能材料工学科 荒井研究室',
            'description' => '機能性材料の研究を行う研究室です。',
            'url' => 'https://example.com/lab5',
            'professor_url' => 'https://example.com/professor5',
            'gender_ratio_male' => 4,
            'gender_ratio_female' => 6,
            'created_by' => $adminId, // 追加
        ]);
    }
}

再度マイグレーションを実行ですね。
エラーが直っていることを祈ります。

実行コマンド(再び)

/var/www
$ php artisan migrate:fresh --seed

コントローラーを作成する

参考記事に従って、コントローラーを作成しましょうか!

実行コマンド

/var/www
$ php artisan make:controller Auth/GoogleAuthController
\project-root\src\app\Http\Controllers\Auth\GoogleAuthController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class GoogleAuthController extends Controller
{
    public function redirect()
    {
        return Socialite::driver('google')->redirect();
    }

    public function callback()
    {
        try {
            $g = Socialite::driver('google')->user();
        } catch (\Throwable $e) {
            return redirect()->route('login')->with('status', 'Google認証に失敗しました。もう一度お試しください。');
        }

        // メール一致で既存ユーザーに紐付け(重複防止)
        $user = User::updateOrCreate(
            ['email' => $g->getEmail()],
            [
                'name' => $g->getName() ?: $g->getNickname() ?: 'Google User',
                'google_id' => $g->getId(),
                // 非NULL制約ならダミーPWを入れる(使われない)
                'password' => Hash::make(Str::random(40)),
                'email_verified_at' => now(),
            ]
        );

        Auth::login($user, remember: true);

        return redirect()->intended(route('labs.home'));
    }
}

作成したメソッドは二つだけです。

一つ目は、redirect()
Socialite::driver('google')->redirect();と一行書くだけで、GoogleのOAuth認証URLを生成して、リダイレクトまでしてくれます。

二つ目のcallback()には、リダイレクトされた先でユーザーが認証操作をするとGoogleから返される認証コードを受け取って、そこからユーザー情報を取得する処理が書かれています。

$g = Socialite::driver('google')->user();のように書くことで、認証されたユーザーの情報を変数に代入することができます。
これにより、例えば以下のようにすることで、ユーザーの情報を取得することができます。

$g->getId();        // GoogleのユーザーID
$g->getName();      // 名前(例: 山田太郎)
$g->getEmail();     // メールアドレス
$g->getAvatar();    // プロフィール画像URL

実際には、以下のようにupdateOrCreate()メソッドでemailの重複を防いで登録します。
これによって、新規登録とログインの両方に対応することができます。

\project-root\src\app\Http\Controllers\Auth\GoogleAuthController.php
        $user = User::updateOrCreate(
            ['email' => $g->getEmail()],
            [
                'name' => $g->getName() ?: $g->getNickname() ?: 'Google User',
                'google_id' => $g->getId(),
                // 非NULL制約ならダミーPWを入れる(使われない)
                'password' => Hash::make(Str::random(40)),
                'email_verified_at' => now(),
            ]
        );

        Auth::login($user, remember: true);

また、パスワードはGoogleからは取得できない一方で、データベースには構造的には登録する必要があるため、ダミーデータをランダムで生成して挿入するようにしています。

ルーティングを追加する

認証必須を実現しているミドルウェアグループの外に追加してください。

\project-root\src\routes\web.php
// 追加: ソーシャルログイン
Route::get('/auth/google', [GoogleAuthController::class, 'redirect'])->name('auth.google');
Route::get('/auth/google/callback', [GoogleAuthController::class, 'callback'])->name('auth.google.callback');

Reactコンポーネントを作成する

新規登録画面とログイン画面を以下のように修正します。

\project-root\src\resources\js\Pages\Auth\Register.jsx
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, Link, useForm } from '@inertiajs/react';

export default function Register() {
    const { data, setData, post, processing, errors, reset } = useForm({
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
    });

    const submit = (e) => {
        e.preventDefault();

        post(route('register'), {
            onFinish: () => reset('password', 'password_confirmation'),
        });
    };

    return (
        <GuestLayout>
            <Head title="Register" />

            <form onSubmit={submit}>
                <div>
                    <InputLabel htmlFor="name" value="Name" />

                    <TextInput
                        id="name"
                        name="name"
                        value={data.name}
                        className="mt-1 block w-full"
                        autoComplete="name"
                        isFocused={true}
                        onChange={(e) => setData('name', e.target.value)}
                        required
                    />

                    <InputError message={errors.name} className="mt-2" />
                </div>

                <div className="mt-4">
                    <InputLabel htmlFor="email" value="Email" />

                    <TextInput
                        id="email"
                        type="email"
                        name="email"
                        value={data.email}
                        className="mt-1 block w-full"
                        autoComplete="username"
                        onChange={(e) => setData('email', e.target.value)}
                        required
                    />

                    <InputError message={errors.email} className="mt-2" />
                </div>

                <div className="mt-4">
                    <InputLabel htmlFor="password" value="Password" />

                    <TextInput
                        id="password"
                        type="password"
                        name="password"
                        value={data.password}
                        className="mt-1 block w-full"
                        autoComplete="new-password"
                        onChange={(e) => setData('password', e.target.value)}
                        required
                    />

                    <InputError message={errors.password} className="mt-2" />
                </div>

                <div className="mt-4">
                    <InputLabel
                        htmlFor="password_confirmation"
                        value="Confirm Password"
                    />

                    <TextInput
                        id="password_confirmation"
                        type="password"
                        name="password_confirmation"
                        value={data.password_confirmation}
                        className="mt-1 block w-full"
                        autoComplete="new-password"
                        onChange={(e) =>
                            setData('password_confirmation', e.target.value)
                        }
                        required
                    />

                    <InputError
                        message={errors.password_confirmation}
                        className="mt-2"
                    />
                </div>

                <div className="mt-4 flex items-center justify-end">
                    <Link
                        href={route('login')}
                        className="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
                    >
                        Already registered?
                    </Link>

                    <PrimaryButton className="ms-4" disabled={processing}>
                        Register
                    </PrimaryButton>
                </div>

                {/* Google ログイン追加部分 */}
                <div className="my-6">
                    <div className="relative my-4">
                        <div className="absolute inset-0 flex items-center">
                            <span className="w-full border-t" />
                        </div>
                        <div className="relative flex justify-center text-xs uppercase">
                            <span className="bg-white px-2 text-gray-500">
                                or
                            </span>
                        </div>
                    </div>

                    <a
                        href={
                            typeof route === 'function'
                                ? route('auth.google')
                                : '/auth/google'
                        }
                        className="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-gray-50 transition"
                    >
                        <svg
                            width="18"
                            height="18"
                            viewBox="0 0 533.5 544.3"
                            aria-hidden="true"
                        >
                            <path
                                fill="#4285f4"
                                d="M533.5 278.4c0-18.5-1.7-36.3-4.9-53.5H272.1v101h146.9c-6.3 34.1-25.6 62.9-54.6 82.2v68h88.2c51.6-47.5 80.9-117.6 80.9-197.7z"
                            />
                            <path
                                fill="#34a853"
                                d="M272.1 544.3c73.3 0 134.9-24.2 179.9-65.2l-88.2-68c-24.5 16.5-55.9 26.1-91.7 26.1-70.6 0-130.4-47.6-151.8-111.6H30.8v70.2c44.8 88.8 136.6 148.5 241.3 148.5z"
                            />
                            <path
                                fill="#fbbc05"
                                d="M120.3 325.6c-10.1-30.1-10.1-62.7 0-92.8v-70.2H30.8c-41.4 82.8-41.4 180.5 0 263.3l89.5-70.3z"
                            />
                            <path
                                fill="#ea4335"
                                d="M272.1 106.3c38.8-.6 76.1 13.7 104.5 39.9l78.1-78.1C407 .8 343-18.1 272.1 18.4 167.4 18.4 75.6 78.2 30.8 167l89.5 70.2c21.4-64 81.1-110.9 151.8-110.9z"
                            />
                        </svg>
                        <span>Googleで登録 / ログイン</span>
                    </a>
                </div>
            </form>
        </GuestLayout>
    );
}

\project-root\src\resources\js\Pages\Auth\Login.jsx
import Checkbox from '@/Components/Checkbox';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, Link, useForm } from '@inertiajs/react';

export default function Login({ status, canResetPassword }) {
    const { data, setData, post, processing, errors, reset } = useForm({
        email: '',
        password: '',
        remember: false,
    });

    const submit = (e) => {
        e.preventDefault();

        post(route('login'), {
            onFinish: () => reset('password'),
        });
    };

    return (
        <GuestLayout>
            <Head title="Log in" />

            {status && (
                <div className="mb-4 text-sm font-medium text-green-600">
                    {status}
                </div>
            )}

            <form onSubmit={submit}>
                <div>
                    <InputLabel htmlFor="email" value="Email" />

                    <TextInput
                        id="email"
                        type="email"
                        name="email"
                        value={data.email}
                        className="mt-1 block w-full"
                        autoComplete="username"
                        isFocused={true}
                        onChange={(e) => setData('email', e.target.value)}
                    />

                    <InputError message={errors.email} className="mt-2" />
                </div>

                <div className="mt-4">
                    <InputLabel htmlFor="password" value="Password" />

                    <TextInput
                        id="password"
                        type="password"
                        name="password"
                        value={data.password}
                        className="mt-1 block w-full"
                        autoComplete="current-password"
                        onChange={(e) => setData('password', e.target.value)}
                    />

                    <InputError message={errors.password} className="mt-2" />
                </div>

                <div className="mt-4 block">
                    <label className="flex items-center">
                        <Checkbox
                            name="remember"
                            checked={data.remember}
                            onChange={(e) =>
                                setData('remember', e.target.checked)
                            }
                        />
                        <span className="ms-2 text-sm text-gray-600">
                            Remember me
                        </span>
                    </label>
                </div>

                <div className="mt-4 flex items-center justify-end">
                    {canResetPassword && (
                        <Link
                            href={route('password.request')}
                            className="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
                        >
                            Forgot your password?
                        </Link>
                    )}

                    <PrimaryButton className="ms-4" disabled={processing}>
                        Log in
                    </PrimaryButton>
                </div>

                {/* Google ログイン追加部分 */}
                <div className="my-6">
                    <div className="relative my-4">
                        <div className="absolute inset-0 flex items-center">
                            <span className="w-full border-t" />
                        </div>
                        <div className="relative flex justify-center text-xs uppercase">
                            <span className="bg-white px-2 text-gray-500">
                                or
                            </span>
                        </div>
                    </div>

                    <a
                        href={
                            typeof route === 'function'
                                ? route('auth.google')
                                : '/auth/google'
                        }
                        className="inline-flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-gray-50 transition"
                    >
                        <svg
                            width="18"
                            height="18"
                            viewBox="0 0 533.5 544.3"
                            aria-hidden="true"
                        >
                            <path
                                fill="#4285f4"
                                d="M533.5 278.4c0-18.5-1.7-36.3-4.9-53.5H272.1v101h146.9c-6.3 34.1-25.6 62.9-54.6 82.2v68h88.2c51.6-47.5 80.9-117.6 80.9-197.7z"
                            />
                            <path
                                fill="#34a853"
                                d="M272.1 544.3c73.3 0 134.9-24.2 179.9-65.2l-88.2-68c-24.5 16.5-55.9 26.1-91.7 26.1-70.6 0-130.4-47.6-151.8-111.6H30.8v70.2c44.8 88.8 136.6 148.5 241.3 148.5z"
                            />
                            <path
                                fill="#fbbc05"
                                d="M120.3 325.6c-10.1-30.1-10.1-62.7 0-92.8v-70.2H30.8c-41.4 82.8-41.4 180.5 0 263.3l89.5-70.3z"
                            />
                            <path
                                fill="#ea4335"
                                d="M272.1 106.3c38.8-.6 76.1 13.7 104.5 39.9l78.1-78.1C407 .8 343-18.1 272.1 18.4 167.4 18.4 75.6 78.2 30.8 167l89.5 70.2c21.4-64 81.1-110.9 151.8-110.9z"
                            />
                        </svg>
                        <span>Googleでログイン</span>
                    </a>
                </div>
            </form>
        </GuestLayout>
    );
}

動作確認をする

まずは、新規登録画面を開きます。
http://localhost/register
image.png

追加された「Googleで登録/ログイン」をクリックします。
すると、Googleの認証ページにリダイレクトされることが確認できます。
image.png

ダッシュボードにアクセスると、ログインできていることが確認できます。
一度ログアウトします。
http://localhost/dashboard
image.png

今度はログインをしてみましょう。
例によって、EmailとPasswordの入力は不要です。
http://localhost/login
image.png

ログインできましたね!
image.png

4. まとめ・次回予告

お疲れ様でした!
ついに、長かったバックエンド実装編も今日でおしまいです。
まだまだ直すべきところはたくさんあるかと思いますが、先に進まないといけませんね!

今回は、Googele認証によって、メールアドレスとパスワードの登録なして新規登録・ログインできるソーシャルログイン機能を実装しました。
それに際して、OAuthを利用するために初めてGoolge Cloudを使用しました。
また、実装にはLaravel Socialiteを用いました。

次回から、フロントエンド実装編ということで、今までテキトーだった画面のデザインやボタンの機能などを作っていきたいと思います!

ただ、記事のストックが完全に尽きてしまっている状態なので、2週間ほどお休みをいただきます。(笑)

また、次回お会いしましょう!
ここまで読んでくださって本当にありがとうございました!
引き続きよろしくお願いいたします。(*´ω`)

これまでの記事一覧

--- 要件定義・設計編 ---

--- 環境構築編 ---

--- バックエンド実装編 ---

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?