Laravel Sail Sanctum

パクリ36発目!!
まだ少しやる気が続いているのでそのまま続ける!
パクリ元
内容
前回作った環境 (Laravel sail + react) でSPA認証ログインまでをやりたい。
Sanctum
パクリ元1つ目を参考に Laravel Sanctum に対応する。
ドメイン設定
laravel sail としては localhost があればいいのでそのまま。
Middleware設定
・・・
->withMiddleware(function (Middleware $middleware) {
$middleware->statefulApi();
})
・・・
CORS設定
laravel sail なので無視。
ユーザー作成
tinker とやらのコマンドラインで作成する。
~/example-app$ sail artisan tinker
Psy Shell v0.12.8 (PHP 8.4.5 — cli) by Justin Hileman
> use App\Models\User;
> User::factory(3)->create();
= Illuminate\Database\Eloquent\Collection {#5248
・・・
> q
phpmyadmin(localhost:8888)でユーザー3件登録を確認。
コントローラー作成
全パクリ。
~/example-app$ sail artisan make:controller Api/BaseController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Collection;
class BaseController extends Controller
{
/**
* success response method
*
* @param string $message
* @param array<string, mixed>|Collection $data
* @return \Illuminate\Http\JsonResponse
*/
public function sendResponse(string $message, array|Collection $data)
{
$response = [
'success' => true,
'message' => $message,
'data' => $data,
];
return response()->json($response, 200);
}
/**
* return error response.
*
* @param string $message
* @param MessageBag|array<int|string, mixed> $errorMessages = []
* @param int $code = 404
* @return \Illuminate\Http\JsonResponse
*/
public function sendError(
string $message,
MessageBag|array $data = [],
int $code = 404
) {
$response = [
'success' => false,
'message' => $message,
];
if (!empty($data)) {
$response['data'] = $data;
}
return response()->json($response, $code);
}
}
~/example-app$ sail artisan make:controller Api/LoginController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Api\BaseController;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Validator;
class LoginController extends BaseController
{
/**
* returns default response.
* route: get('/api/login')
*
* @return \Illuminate\Http\JsonResponse
*/
public function create()
{
return $this->sendError(
'Authentication Required.',
[],
401,
);
}
/**
* authenticate with credentials.
* route: post('/api/login')
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'email'],
'password' => ['required'],
]);
if ($validator->fails()) {
return $this->sendError(
'Bad Request',
[$validator->errors()],
400,
);
}
$credentials = $request->only(['email', 'password']);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended(route('api.loggedin'));
}
return $this->sendError(
'Login Failed',
['email' => 'The provided credentials do not match our records.'],
401,
);
}
/**
* returns response after after login redirect.
* route: get('/api/loggedin')
*
* @return \Illuminate\Http\JsonResponse
*/
public function loggedin()
{
return $this->sendResponse(
'Logged in.',
['email' => 'Authenticated.'],
);
}
}
なんかバリデーション箇所うまくいってなかったので修正。
~/example-app$ sail artisan make:controller Api/UsersController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Api\BaseController;
use App\Models\User;
use Illuminate\Http\Request;
class UsersController extends BaseController
{
/**
* returns list of users.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$users = User::all();
return $this->sendResponse('successfully fetched.', $users);
}
}
~/example-app$ sail artisan make:controller SpaController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class SpaController extends Controller
{
/**
* returns spa.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('spa.index');
}
}
ビュー作成
よく見たら Vue.js を使用している様子。。パクリ元2つ目も参考に、以前やった react のログインに適当に置き換えてみる。
~/example-app$ sail artisan make:view spa/index
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SPA | Stateful Authentication with Sanctum</title>
{{-- react に変更があったとき自動で --}}
@viteReactRefresh
@vite(['resources/sass/app.scss', 'resources/ts/index.tsx'])
</head>
<body>
<div id="root"></div>
</body>
</html>
index.tsx、Login.tsx、Dashboard.tsx を作成。
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Route, Routes, BrowserRouter } from "react-router-dom";
import { Login } from "./Login";
import { Dashboard } from "./Dashboard";
createRoot(document.getElementById('root') as Element).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
</StrictMode>
);
~/example-app$ sail npm install -D @mui/material @emotion/react @emotion/styled
import {
Box,
Button,
Card,
CardActions,
CardContent,
CardHeader,
TextField,
} from "@mui/material";
import { useState } from 'react';
import { useNavigate } from "react-router-dom";
import axios from 'axios';
export const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const cardStyle = {
display: "block",
transitionDuration: "0.3s",
height: "450px",
width: "400px",
variant: "outlined",
};
const fetchUsers = async () => {
http.get('/api/users').then((res2) => {
console.log(res2.data.data);
}).catch((err) => {
console.log("ユーザー取得に失敗しました。")
})
}
const http = axios.create({
baseURL: 'http://localhost',
withCredentials: true,
});
const onClickLogin = () => {
const auth = async () => {
http.get('/sanctum/csrf-cookie').then((res1) => {
http.post('/api/login', {email, password}).then((res2) => {
if (res2.status == 200) {
console.log("ログインに成功しました。");
navigate("/dashboard");
} else {
console.log("ログインに失敗しました。");
}
}).catch((err) => {
console.log("ログインに失敗しました。")
})
}).catch((err) => {
console.log("ログインに失敗しました。")
})
}
auth();
};
return (
<Box
display="flex"
alignItems="center"
justifyContent="center"
padding={20}
>
<Card style={cardStyle}>
<CardHeader title="ログインページ" />
<CardContent>
<div>
<TextField
fullWidth
id="username"
type="email"
label="Username"
placeholder="Username"
margin="normal"
onChange={(e) => setEmail(e.target.value)}
/>
<TextField
fullWidth
id="password"
type="password"
label="Password"
placeholder="Password"
margin="normal"
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</CardContent>
<CardActions>
<Button
variant="contained"
size="large"
color="secondary"
onClick={onClickLogin}
>
Login
</Button>
</CardActions>
</Card>
</Box>
);
};
import {
Box,
Card,
CardContent,
CardHeader,
} from "@mui/material";
import { useState, useEffect } from 'react';
import axios from 'axios';
type User = {
id: string;
name: string;
};
const cardStyle = {
display: "block",
transitionDuration: "0.3s",
height: "450px",
width: "400px",
variant: "outlined",
};
export const Dashboard = () => {
const [users, setUsers] = useState<User[]>([]);
const http = axios.create({
baseURL: 'http://localhost',
withCredentials: true,
});
const fetchUsers = async () => {
http.get('/api/users').then((res2) => {
setUsers(res2.data.data);
}).catch((err) => {
console.log("ユーザー取得に失敗しました。")
})
}
useEffect(() =>{
fetchUsers();
},[]);
return (
<Box
display="flex"
alignItems="center"
justifyContent="center"
padding={20}
>
<Card style={cardStyle}>
<CardHeader title="User Name" />
<CardContent>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</CardContent>
</Card>
</Box>
);
};
いつの間にかタブ数がおかしくなってる気もするが、、
ルーティング
<?php
use App\Http\Controllers\Api\LoginController;
use App\Http\Controllers\Api\UsersController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
Route::get('/login', [LoginController::class, 'create'])->name('login');
Route::post('/login', [LoginController::class, 'login'])->name('api.login.store');
Route::middleware('auth:sanctum')->group(function () {
Route::get('/loggedin', [LoginController::class, 'loggedin'])->name('api.loggedin');
Route::get('/users', [UsersController::class, 'index'])->name('api.users');
});
<?php
use App\Http\Controllers\SpaController;
use Illuminate\Support\Facades\Route;
Route::get('/login', [SpaController::class, 'index'])->name('spa');
確認
http://localhost/api/login 、または、http://localhost/api/users にブラウザでアクセスして、{“success":false,"message":"Authentication Required."} が表示されることを確認。

http://localhost/login から users テーブルに入っているデータの email カラムの値と password という値でログインボタン押下してユーザー一覧画面が表示されることを確認。


とりあえずOK。
はじめ fetch でやってたけど XSRF-TOKEN が返ってきても自分でその値を取り出して X-XSRF-TOKEN をくっつけないといけなかったりして axios に変更したりで結構時間がかかる。
雑感
前回払い出したレスポンスクッキーを認証時にリクエストクッキーとして渡して認証するというのが Sanctum の機能ということを理解。
細かい所が違ってたりして動かないことが結構ある。情報が古いからなのか元からおかしかったのかよく分からんが AI の情報集めもどうしてるんだろと思う。英語情報だと正確なのかも知らんが周回遅れでやってると最新情報を集める必要がないのでどうしても日本語情報に頼っちゃうよねー
ディスカッション
コメント一覧
まだ、コメントがありません