Commit 3d29669b authored by Egor Kremnev's avatar Egor Kremnev

add laravel passport. add register and auth in frontend

parent 76bc2b67
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\RegisterRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PassportAuthController extends Controller
{
/**
* @param RegisterRequest $request
* @return JsonResponse
*/
public function register(RegisterRequest $request): JsonResponse
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password)
]);
return response()->json(
['token' => $user->createToken('LaravelAuthApp')->accessToken],
201
);
}
/**
* @param Request $request
* @return JsonResponse
*/
public function login(Request $request): JsonResponse
{
if (auth()->attempt($request->only(['email', 'password']))) {
return response()
->json(['token' => auth()->user()->createToken('LaravelAuthApp')->accessToken]);
}
return response()->json(['error' => 'Unauthorised'], 401);
}
}
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'name' => 'required|min:4',
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
];
}
}
...@@ -2,13 +2,11 @@ ...@@ -2,13 +2,11 @@
namespace App\Models; namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Passport\HasApiTokens;
class User extends Authenticatable class User extends Authenticatable
{ {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
...@@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider ...@@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
// Passport::ignoreRoutes();
} }
/** /**
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
"php": "^8.1", "php": "^8.1",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10", "laravel/framework": "^10.10",
"laravel/passport": "^11.8",
"laravel/sanctum": "^3.2", "laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8" "laravel/tinker": "^2.8"
}, },
......
This diff is collapsed.
...@@ -40,6 +40,10 @@ return [ ...@@ -40,6 +40,10 @@ return [
'driver' => 'session', 'driver' => 'session',
'provider' => 'users', 'provider' => 'users',
], ],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
], ],
/* /*
......
...@@ -64,4 +64,5 @@ return [ ...@@ -64,4 +64,5 @@ return [
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
], ],
'routes' => false
]; ];
...@@ -14,15 +14,8 @@ use Illuminate\Support\Facades\Route; ...@@ -14,15 +14,8 @@ use Illuminate\Support\Facades\Route;
| |
*/ */
Route::middleware('auth:sanctum')->get('/user', function (Request $request) { Route::post('register', [App\Http\Controllers\Api\PassportAuthController::class, 'register']);
return $request->user(); Route::post('login', [App\Http\Controllers\Api\PassportAuthController::class, 'login']);
});
Route::resource('/articles', \App\Http\Controllers\Api\ArticleController::class) Route::resource('/articles', \App\Http\Controllers\Api\ArticleController::class)
->only('index'); ->middleware('auth:api');
//Route::resource('/articles', \App\Http\Controllers\Api\ArticleController::class)
// ->except('edit', 'create');
//Route::resource('/articles/{article}/comments', \App\Http\Controllers\Api\CommentController::class)
// ->except('edit', 'create');
...@@ -2,11 +2,26 @@ ...@@ -2,11 +2,26 @@
namespace Tests; namespace Tests;
use App\Models\User;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\WithFaker;
use Laravel\Passport\Passport;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
use CreatesApplication, RefreshDatabase, WithFaker; use CreatesApplication, RefreshDatabase, WithFaker;
/** @var User $user */
protected $user;
/**
* @return void
*/
public function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
Passport::actingAs($this->user);
}
} }
import React from "react";
import {Message} from "semantic-ui-react";
const Errors = ({errorMessage, errors}) => {
return <Message negative>
<Message.Header>{errorMessage}</Message.Header>
<div>
{
Object.keys(errors).map(key => (
<div key={key}>
<div>{key.charAt(0).toUpperCase() + key.slice(1)}</div>
<ul className="list">
{errors[key].map(error => (<li key={error} className="content">{error}</li>))}
</ul>
</div>
)
)
}
</div>
</Message>
};
export default Errors;
import React, {Component} from "react"; import React, {useEffect, useState} from "react";
import {Container, Menu} from "semantic-ui-react"; import {Container, Menu} from "semantic-ui-react";
import {Link} from "react-router-dom"; import {Link, useLocation, useNavigate} from "react-router-dom";
export default class Navbar extends Component { const Navbar = () => {
render() { const [state, setState] = useState({
isAuth: false,
activeItem: 'home'
});
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setState(prevState => ({...prevState, isAuth: true}));
}
}, [location]);
const handleItemClick = (e, { name }) => setState(prevState => ({...prevState, activeItem: name }));
const handleLogOut = (e) => {
setState({ isAuth: false, activeItem: 'logout' });
localStorage.removeItem('token');
navigate("/login");
};
const authButtons = () => {
return ( return (
<div> <>
<Menu fixed="top" inverted> <Menu.Item
<Container> as={Link} to="/login"
<Menu.Item name='login'
as={Link} active={state.activeItem === 'login'}
header to="/">Home</Menu.Item> onClick={handleItemClick}
<Menu.Item />
as={Link} <Menu.Item
to="/articles" id="articles-button"> as={Link} to="/register"
name='register'
active={state.activeItem === 'register'}
onClick={handleItemClick}
/>
</>
);
};
const logOutButton = () => {
return (
<>
<Menu.Item
name='logout'
active={state.activeItem === 'logout'}
onClick={handleLogOut}
/>
</>
)
};
return (
<div>
<Menu fixed="top" inverted>
<Container>
<Menu.Item name='home' active={state.activeItem === 'home'} as={Link} to="/" header onClick={handleItemClick}>
Home
</Menu.Item>
{
state.isAuth &&
<Menu.Item name='articles' active={state.activeItem === 'articles'}
as={Link} to="/articles" onClick={handleItemClick}>
Articles Articles
</Menu.Item> </Menu.Item>
</Container> }
</Menu> <Menu.Menu position='right'>
</div> {state.isAuth ? logOutButton() : authButtons()}
); </Menu.Menu>
} </Container>
} </Menu>
</div>
);
};
export default Navbar;
import React, {Component} from "react"; import React, {useEffect, useState} from "react";
import './ArticlesPage.css'; import './ArticlesPage.css';
import {API_BASE_URL} from "../../config"; import {API_BASE_URL} from "../../config";
import {Header, Message} from "semantic-ui-react"; import {Header, Message} from "semantic-ui-react";
import ArticlesPageTable from "../../components/ArticlesPageTable/ArticlesPageTable"; import ArticlesPageTable from "../../components/ArticlesPageTable/ArticlesPageTable";
import {useNavigate} from "react-router-dom";
import {get} from "../../services/request";
export default class ArticlesPage extends Component { const ArticlesPage = () => {
constructor(props) { const [state, setState] = useState({
super(props); articles: null,
this.state = { isLoading: false
articles: null, });
isLoading: false
} const navigate = useNavigate();
}
const getArticles = () => {
const token = localStorage.getItem('token');
if (!token) navigate('/login');
async getArticles() { try {
if (!this.state.articles) { setState(prevState => ({...prevState, isLoading: true}));
try { const response = get(API_BASE_URL + "/articles", token);
this.setState({isLoading: true}); response.then(data => setState({articles: data.data, isLoading: false}));
const response = await fetch( } catch (e) {
API_BASE_URL + '/articles', setState(prevState => ({...prevState, isLoading: false}));
{ console.error(e);
headers: {
Accept: "application/json"
}
}
)
const articlesData = await response.json();
this.setState({articles: articlesData.data, isLoading: false});
} catch (e) {
this.setState({isLoading: false});
console.error(e);
}
} }
} }
componentDidMount() { useEffect(() => {
this.getArticles(); const token = localStorage.getItem('token');
}
if (!token) navigate('/login');
if (!state.articles) getArticles();
}, []);
return (
<div>
<Header as="h1">Articles</Header>
{state.isLoading && <Message info header="Loading articles..." />}
{state.articles && <ArticlesPageTable articles={state.articles} />}
</div>
);
};
export default ArticlesPage;
render() {
return (
<div>
<Header as="h1">Articles</Header>
{this.state.isLoading && <Message info header="Loading articles..." />}
{this.state.articles && <ArticlesPageTable articles={this.state.articles} />}
</div>
);
}
}
import React, {useEffect, useState} from 'react';
import {Button, Form, Message} from 'semantic-ui-react';
import {API_BASE_URL} from "../../../config";
import {post} from "../../../services/request";
import {useNavigate} from "react-router-dom";
import Errors from "../../../components/Errors/Errors";
const Login = () => {
const [state, setState] = useState({
email: null,
password: null,
isLoading: false,
errorMessage: null,
errors: null,
error: null
});
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
navigate("/");
}
}, []);
const handleSubmit = (e) => {
setState(prevState => ({...prevState, isLoading: true}));
const data = {
email: state.email,
password: state.password
};
const res = post(API_BASE_URL + "/login", data);
res.then(response => {
if (Object.keys(response).includes('token')) {
localStorage.setItem('token', response.token);
navigate('/');
} else if (Object.keys(response).includes('error')) {
setState(prevState => ({...prevState, error: response.error}));
}
setState(prevState => ({
...prevState,
errorMessage: response.message,
errors: response.errors,
isLoading: false
}));
}
);
};
const onChangeInputHandler = e => {
const {name, value} = e.currentTarget;
setState(prevState => ({
...prevState,
[name]: value,
errors: null
}));
};
return (
<div>
{state.error && <Message negative><Message.Header>{state.error}</Message.Header></Message>}
{state.errors && <Errors errors={state.errors} errorMessage={state.errorMessage}/>}
<Form>
<Form.Field>
<label>Email</label>
<input type="email" placeholder='Email' name='email' onChange={onChangeInputHandler}/>
</Form.Field>
<Form.Field>
<label>Password</label>
<input type="password" placeholder='Password' name='password' onChange={onChangeInputHandler}/>
</Form.Field>
<Button loading={state.isLoading} type='submit' onClick={(event) => handleSubmit(event)}>
Login
</Button>
</Form>
</div>
);
};
export default Login;
import React, {useEffect, useState} from 'react';
import {Button, Form} from 'semantic-ui-react';
import {API_BASE_URL} from "../../../config";
import {post} from "../../../services/request";
import {useNavigate} from "react-router-dom";
import Errors from "../../../components/Errors/Errors";
const Register = () => {
const [state, setState] = useState({
name: null,
email: null,
password: null,
passwordConfirmation: null,
isLoading: false,
errors: null
});
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
navigate("/");
}
}, []);
const handleSubmit = (e) => {
setState(prevState => ({...prevState, isLoading: true}));
const data = {
name: state.name,
email: state.email,
password: state.password
};
const res = post(API_BASE_URL + "/register", data);
res.then(response => {
if (Object.keys(response).includes('token')) {
localStorage.setItem('token', response.token);
navigate('/');
}
setState(prevState => ({
...prevState,
errorMessage: response.message,
errors: response.errors,
isLoading: false
}));
});
};
const onChangeInputHandler = e => {
const {name, value} = e.currentTarget;
console.log(name,value);
setState(prevState => ({
...prevState,
[name]: value,
errors: null
}));
};
return (
<div>
{state.errors && <Errors errors={state.errors} errorMessage={state.errorMessage}/>}
<Form>
<Form.Field>
<label>Name</label>
<input type="text" placeholder='Name' name='name' onChange={onChangeInputHandler}/>
</Form.Field>
<Form.Field>
<label>Email</label>
<input type="email" placeholder='Email' name='email' onChange={onChangeInputHandler}/>
</Form.Field>
<Form.Field>
<label>Password</label>
<input type="password" placeholder='Password' name='password' onChange={onChangeInputHandler}/>
</Form.Field>
<Button loading={state.isLoading} type='submit' onClick={handleSubmit}>
Register
</Button>
</Form>
</div>
);
}
export default Register;
...@@ -4,6 +4,8 @@ import Root from "./Root"; ...@@ -4,6 +4,8 @@ import Root from "./Root";
import React from 'react'; import React from 'react';
import Error from "./Error"; import Error from "./Error";
import ArticlesPage from "../containers/ArticlesPage/ArticlesPage"; import ArticlesPage from "../containers/ArticlesPage/ArticlesPage";
import Register from "../containers/Auth/Register/Register";
import Login from "../containers/Auth/Login/Login";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
...@@ -18,6 +20,14 @@ const router = createBrowserRouter([ ...@@ -18,6 +20,14 @@ const router = createBrowserRouter([
{ {
path: '/articles', path: '/articles',
element: <ArticlesPage /> element: <ArticlesPage />
},
{
path: '/register',
element: <Register />
},
{
path: '/login',
element: <Login />
} }
] ]
} }
......
function playWithResponsePromise(response) {
return new Promise(function (resolve, reject) {
if (response.status < 400) response.json().then(resolve);
else response.json().then(resolve);
});
}
export function get(url, token) {
return fetch(
url,
{
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-type': 'application/json',
...token && {'Authorization': 'Bearer ' + token}
}
}
).then(playWithResponsePromise);
}
export function post(url, data, token) {
return fetch(
url,
{
method: 'POST',
body: JSON.stringify(data),
headers: {
'Accept': 'application/json',
'Content-type': 'application/json',
...token && {'Authorization': 'Bearer ' + token}
}
}
).then(playWithResponsePromise);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment