I'm setting up a laravel, vue project and i am using JWT auth for the user authentication. I am trying to protect the Routes with Vue Router and it is getting token from the local storage and giving access to the authentic user to specific route, but once on another route if i click on any other route or refresh the page it redirects me on the "/login" page again. The token remains same all the time but it is considering the token as the user is not authentic. Please help as i am new to laravel and vue
I have tried using meta info but that didn't work as well. Moreover, i have tried deleting the token from local storage and created it again but nothing works for me.
routes.js file
export const routes = [
{
path: '/', component: Home, name: 'home'
},
{
path: '/dashboard', component: Dashboard, name: 'dashboard', meta:{requiresAuth: true}
},
{
path: '/login', component: Login, name: 'login'
}
];
App.js file
import VueRouter from 'vue-router';
import { routes } from './routes.js';
window.Vue = require('vue');
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
routes
});
router.beforeEach((to, from, next) => {
console.log(to)
if (to.meta.requiresAuth) {
const authUser = JSON.stringify(window.localStorage.getItem('usertoken'))
if(authUser && authUser.accessToken){
console.log("here")
next();
}else{
next({name: 'login'});
}
}
next();
});
const app = new Vue({
el: '#app',
router
});
I expect the output to be like when the user is authentic and the router.beforeEach method finds a token, the user can get to any route until the token gets deleted or changed. Moreover, the user should not be taken to '/login' everytime a <router-link> is clicked or page is refreshed.
I was just trying to solve it and it is solved...the problem was with the line if(authUser && authUser.accessToken) . I added authUser.accessToken as a condition which was not fulfilled, so it was redirecting on every click. I removed that condition and just left with if(authUser) and now it is working perfectly. Also I have added JSON.stringify to change my object to text and then authenticate with JSON.parse by passing a variable.
My final code looks like:-
router.beforeEach((to, from, next) => {
console.log(to)
if (to.meta.requiresAuth) {
var usertoken = JSON.stringify(window.localStorage.getItem('usertoken'))
const authUser = JSON.parse(usertoken)
if(authUser){
console.log("here")
next();
}else{
next({name: 'login'});
}
}
next();
});
Related
I am trying to get React and Laravel to work together using the middleware Sanctum.
I can read many examples of people trying to do this with a cookie based setup, but I am trying to use the token setup for a pure API approach. I am doing this because I want to prepare the backend for use in a mobile app where cookies are not available.
This is a part of my setup:
/backend/routes/api.php:
Route::post('/login', [ UserController::class, 'getAccessToken'] );
/frontend/store/api.js
static login( user ) {
var formData = new FormData();
formData.append('username', user.username);
formData.append('password', user.password )
formData.append('deviceName', 'browser');
return fetch(
'http://localhost:5001/api/login, {
method : 'post',
body : formData
}
);
}
My problems is that it forces a CSRF token check, when the login route is accessed.
That is even if the login route shouldn't be guarded by Sanctum.
This of course fails when I am logging in and don't yet have a token to attach to the request.
As I understand the token is only needed on accessing guarded routes after login.
I have double checked that it is accessing the right route by renaming it to something fake and getting an error.
Am I doing something wrong with the use of Sanctum or is Sanctum just not the preferred use for api tokens? Should I maybe look into JWT instead?
Thank you in advance for your help. <3
Please, check this url, I was able to make it work thanks to this tutorial.
https://laravel-news.com/using-sanctum-to-authenticate-a-react-spa
here is my LoginForm.jsx
import React from "react";
import apiClient from "./services/apiClient";
const LoginForm = (props) => {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const handleSubmit = (e) => {
e.preventDefault();
apiClient.get("/sanctum/csrf-cookie").then((response) => {
apiClient
.post("/api/sanctum-token", {
email: email,
password: password,
device_name: "React v0.1",
})
.then((response) => {
console.log(response);
});
});
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
name="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Login</button>
</form>
</div>
);
};
export default LoginForm;
apiClient.js
import axios from "axios";
const apiClient = axios.create({
baseURL: "http://localhost",
withCredentials: true,
});
export default apiClient;
In:
/backend/app/Http/Kernel.php
I had added:
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
It worked when I removed that line. I had failed to understand that a SPA equals the use of cookies, because I would say that I am also working on a SPA that just uses API tokens instead.
Next issue is that I am using Doctrine instead of Eloquent, which I can now see it far from compatible with Sanctum when it comes to issuing the token. But that will be a topic for another question.
I use this tutorial https://medium.com/#suton.tamimy/basic-token-authentication-with-fresh-laravel-8-sanctum-and-reactjs-ef14eba7ce0f
default setting laravel (no setting modifications etc) with API Login output TOKEN and BEARER token type. Then in ReactJs, just use this axios setting to generate the token (no need sanctum/csrf-cookie).
axios.defaults.baseURL = '_MY_API_';
axios.post('/login', {
email: '_EMAIL_INPUT_FORM _',
password: '_PASSWORD_INPUT_FORM_'
})
.then(({ data }) => {
if(data.status==="success"){
console.log(data)
} else {
console.log("error")
}
});
});
This is the problem : some vue routes don't work with Laravel router whereas others are working.
This one works :
Route::get('/dishesV2/edit/{id}', function () {
return view('back.nutritionV2.index');
});
Whereas this one does not :
Route::get('/mealPlansV2/edit/{id}', function () {
return view('back.nutritionV2.index');
});
The error thrown when I try to access to the second route is thrown by a route called after the creation of the vue component.. After the component is created, it's calling for the following route :
Route::get('/mealPlansV2/{id}/{day}','MealPlansV2Controller#fetchMealPlanDay');
And when I dd the parameters called by this route, I have the parameters of the route '/mealPlansV2/edit/{id}'...
Second problem is when I call the route columns in Laravel :
Route::get('/mealPlansV2/columns',function () {
return view('back.nutritionV2.index');
});
What's returned is the axios request "fetchMealPlans" that is called inside the components "columns" instead of the vue route columns...
This is how my vue routes look like :
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/mealPlansV2/columns',
name: 'MealPlansColumns',
component: MealPlansColumns,
},
{
path: '/mealPlansV2/edit/:id',
name: 'MealPlansEdit',
component: MealPlansEdit,
},
{
path: '/dishesV2/columns',
name: 'DishesColumns',
component: DishesColumns,
},
{
path: '/dishesV2/edit/:id',
name: 'DishesEdit',
component: DishesEdit,
},
],
});
Only the route DishesEdit works in my app.
Thanks for the help and do not hesitate if I haven't been clear on some points.
I am currently trying to create a simple SPA using Vue and Laravel. I've got the basics to work - users can register and login.
I just can't figure out how to create a logout function.
This is what I currently have:
AuthController.php:
public function logout()
{
$accessToken = auth()->user()->token();
$refreshToken = DB::table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update([
'revoked' => true
]);
$accessToken->revoke();
return response()->json(['status' => 200]);
}
routes/api.php:
Route::middleware('auth:api')->group(function () {
Route::post('/logout', 'API\AuthController#logout');
Route::get('/get-user', 'API\AuthController#getUser');
});
Right now, this is what I have tried to do:
Layout.vue:
methods: {
logout() {
axios.post('/api/logout').then(response => {
this.$router.push("/login")
}).catch(error => {
location.reload();
});
}
}
Which calls my logout function in Auth.js:
logout() {
localStorage.removeItem('token')
localStorage.removeItem('expiration')
}
However, when users click on the logout function, they are not logged out immediately (redirected to the login page) - they can still browse "user only pages".
I have to refresh the page before I am properly logged out.
Can anyone assist me with this? Is this even the right approach to a "secure" logout function?
That's a bit old, however, I've just started with Laravel/Vue and managed to do this quite simple. Using the integrated auth from Laravel, you could just simulate the logout from app.blade.php like so:
<b-dropdown-item href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Sign Out</b-dropdown-item> //that's instead of a regular <a> tag
<b-form id="logout-form" action="logout" method="POST" style="display: none;">
<input type="hidden" name="_token" :value="csrf">
</b-form>
You'll need to have the csrf token passed through data in your script in order for it to work like so:
export default {
data() {
return {
csrf: document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
},
methods: {
submit : function(){
this.$refs.form.submit();
}
}
}
As well as adding a meta csrf in your head (blade.php file) like so:
<meta name="csrf-token" content="{{ csrf_token()}}">
I'm assuming you'll be using the logout in your navbar .vue file
Never used Laravel myself, but you should be able to handle logouts client side without needing to do anything in your backend. At the moment you remove auth token from local storage, so the user loses access to data that requires you to be logged in to get.
You probably call your getUser when you refresh the page and that's why you are only logged out then - you send empty token to your backend server, it can't find a user that's associated to it and returns an empty/default guest object. What's left to do is clear your user state after removing the token in your logout() function or send a request to your /get-user endpoint.
To logout install axios and do the rest code in Laravel 6* / 7* / 8*
npm install axios
the trigger this code when you click on logout
axios.post("logout").then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
For my logout links I'd like to make a request to Laravel to invalidate the current user's (Passport JWT) token.
Here's how I'm doing it:
In my backend:
AuthController.php
I have a logout method:
...
public function logout(Request $request) {
$request->user()->token()->revoke();
return response()->json([
'message' => 'Successfully logged out'
]);
}
routes/api.php
I have a route that can only be accessed by being authenticated.
Route::group(['middleware' => 'auth:api'], function() {
...
Route::get('/logout', 'AuthController#logout');
});
My frontend:
For this I'm using Vue's single file components
App.vue
<template>
<nav>
<ul>
...
<li v-if="isLoggedIn">
<a id="logout-link" href="#" #click.prevent="logout">Logout</a>
</li>
</ul>
</nav>
...
</template>
<script>
export default {
...
methods: {
logout(evt) {
if(confirm("Are you sure you want to log out?")) {
axios.get('api/logout').then(response => {
localStorage.removeItem('auth_token');
// remove any other authenticated user data you put in local storage
// Assuming that you set this earlier for subsequent Ajax request at some point like so:
// axios.defaults.headers.common['Authorization'] = 'Bearer ' + auth_token ;
delete axios.defaults.headers.common['Authorization'];
// If using 'vue-router' redirect to login page
this.$router.go('/login');
})
.catch(error => {
// If the api request failed then you still might want to remove
// the same data from localStorage anyways
// perhaps this code should go in a finally method instead of then and catch
// methods to avoid duplication.
localStorage.removeItem('auth_token');
delete axios.defaults.headers.common['Authorization'];
this.$router.go('/login');
});
}
}
}
}
</script>
The point of this approach then is to invalidate the token on the back end upon logging out. Nevertheless, it might not be necessary to do this if the token has short expiration dates.
Try redirecting with javascript on a successful logout:
window.location.replace("desiredURL");
So I am using Laravel 5.5. I have a data coming from my Controller and I want to pass it to my root vue instance not the component.
So for example I have the Dashboard Controller which has a data of "users"
class DashboardController extends Controller {
public function index(){
$user = User::find(1);
return view('index', compact('user'));
}
}
I am using Larave mix on my project setup. So my main js file is the app.js. That "$user" data I need to pass on the root Vue instance. Which is located in app.js
const app = new Vue({
el: '#dashboard',
data: {
// I want all the data from my controller in here.
},
});
If you don't want to use an API call to get data (using axios or else), you could simply try this :
JavaScript::put(['user' => $user ]);
This will, by default, bind your JavaScript variables to a "footer" view. You should load your app.js after this footer view (or modify param bind_js_vars_to_this_view).
In app.js :
data: {
user: user
}
Read more : https://github.com/laracasts/PHP-Vars-To-Js-Transformer
I would make a request to fetch the user's data as has been suggested.
Alternatively, you can add a prop to the dashboard component in index.blade.php and set the user like <dashboard :user="{{ $user }}"></dashboard>. You'll probably want to json_encode or ->toArray() the $user variable.
Then within the dashboard component you can set data values based on the prop.
props: ['user'],
data () {
return {
user: this.user
}
}
I just solved this by placing a reference on the window Object in the <head> of my layout file, and then picking that reference up with a mixin that can be injected into any component.
TLDR SOLUTION
.env
GEODATA_URL="https://geo.some-domain.com"
config/geodata.php
<?php
return [
'url' => env('GEODATA_URL')
];
resources/views/layouts/root.blade.php
<head>
<script>
window.geodataUrl = "{{ config('geodata.url') }}";
</script>
</head>
resources/js/components/mixins/geodataUrl.js
const geodataUrl = {
data() {
return {
geodataUrl: window.geodataUrl,
};
},
};
export default geodataUrl;
usage
<template>
<div>
<a :href="geodataUrl">YOLO</a>
</div>
</template>
<script>
import geodataUrl from '../mixins/geodataUrl';
export default {
name: 'v-foo',
mixins: [geodataUrl],
data() {
return {};
},
computed: {},
methods: {},
};
</script>
END TLDR SOLUTION
If you want, you can use a global mixin instead by adding this to your app.js entrypoint:
Vue.mixin({
data() {
return {
geodataUrl: window.geodataUrl,
};
},
});
I would not recommend using this pattern, however, for any sensitive data because it is sitting on the window Object.
I like this solution because it doesn't use any extra libraries, and the chain of code is very clear. It passes the grep test, in that you can search your code for "window.geodataUrl" and see everything you need to understand how and why the code is working.
That consideration is important if the code may live for a long time and another developer may come across it.
However, JavaScript::put([]) is in my opinion, a decent utility that can be worth having, but in the past I have disliked how it can be extremely difficult to debug if a problem happens, because you cannot see where in the codebase the data comes from.
Imagine you have some Vue code that is consuming window.chartData that came from JavaScript::put([ 'chartData' => $user->chartStuff ]). Depending on the number of references to chartData in your code base, it could take you a very long time to discover which PHP file was responsible for making window.chartData work, especially if you didn't write that code and the next person has no idea JavaScript::put() is being used.
In that case, I recommend putting a comment in the code like:
/* data comes from poop.php via JavaScript::put */
Then the person can search the code for "JavaScript::put" and quickly find it. Keep in mind "the person" could be yourself in 6 months after you forget the implementation details.
It is always a good idea to use Vue component prop declarations like this:
props: {
chartData: {
type: Array,
required: true,
},
},
My point is, if you use JavaScript::put(), then Vue cannot detect as easily if the component fails to receive the data. Vue must assume the data is there on the window Object at the moment in time it refers to it. Your best bet may be to instead create a GET endpoint and make a fetch call in your created/mounted lifecycle method.
I think it is important to have an explicit contract between Laravel and Vue when it comes to getting/setting data.
In the interest of helping you as much as possible by giving you options, here is an example of making a fetch call using ES6 syntax sugar:
routes/web.php
Route::get('/charts/{user}/coolchart', 'UserController#getChart')->name('user.chart');
app/Http/Controllers/UserController.php
public function getChart(Request $request, User $user)
{
// do stuff
$data = $user->chart;
return response()->json([
'chartData' => $data,
]);
}
Anywhere in Vue, especially a created lifecycle method:
created() {
this.handleGetChart();
},
methods: {
async handleGetChart() {
try {
this.state = LOADING;
const { data } = await axios.get(`/charts/${this.user.id}/coolchart`);
if (typeof data !== 'object') {
throw new Error(`Unexpected server response. Expected object, got: ${data}`);
}
this.chartData = data.chartData;
this.state = DATA_LOADED;
} catch (err) {
this.state = DATA_FAILED;
throw new Error(`Problem getting chart data: ${err}`);
}
},
},
That example assumes your Vue component is a Mealy finite state machine, whereby the component can only be in one state at any given time, but it can freely switch between states.
I'd recommend using such states as computed props:
computed: {
isLoading() { return (this.state === LOADING); },
isDataLoaded() { return (this.state === DATA_LOADED); },
isDataFailed() { return (this.state === DATA_FAILED); },
},
With markup such as:
<div v-show="isLoading">Loading...</div>
<v-baller-chart v-if="isDataLoaded" :data="chartData"></v-baller-chart>
<button v-show="isDataFailed" type="button" #click="handleGetChart">TRY AGAIN</button>
I have a route defined in routes.php file but when i make an ajax request from my angular app, i get this error
{"error":{"type":"Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException","message":"Controller method not found.","file":"C:\\xampp\\htdocs\\tedxph\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Controllers\\Controller.php","line":290}}
this is my routes file
/*
|--------------------------------------------------------------------------
| Api Routes
|--------------------------------------------------------------------------
*/
Route::group(array('prefix' => 'api'), function() {
//Auth Routes
Route::post('auth/login', 'ApiUserController#authUser');
Route::post('auth/signup', 'ApiUserController#registerUser');
/* Persons */
Route::group(array('prefix' => 'people'), function() {
Route::get('{id}', 'ApiPeopleController#read');
Route::get('/', 'ApiPeopleController#read');
});
/* Events */
Route::group(array('prefix' => 'events'), function() {
Route::get('{id}', 'ApiEventsController#read');
Route::get('/','ApiEventsController#read');
});
});
Accessing the same url (http://localhost/site/public/api/auth/signup) from a rest client app on chrome does not give any errors, what could be wrong?
this is the angular code from my controller
$rootScope.show('Please wait..registering');
API.register({email: email, password: password})
.success(function (data) {
if(data.status == "success") {
console.log(data);
$rootScope.hide();
}
})
.error(function (error) {
console.log(error)
$rootScope.hide();
})
more angular code
angular.module('tedxph.API', [])
.factory('API', function ($rootScope, $http, $ionicLoading, $window) {
//base url
var base = "http://localhost/tedxph/public/api";
return {
auth: function (form) {
return $http.post(base+"/auth/login", form);
},
register: function (form) {
return $http.post(base+"/auth/signup", form);
},
fetchPeople: function () {
return $http.get(base+"/people");
},
fetchEvents: function() {
return $http.get(base+"/events");
},
}
});
It'd help to see the code you're using to make the angular request, as well as the header information from Chrome's Network -> XHR logger, but my first guess would be Angular is sending the AJAX request with the GET method instead of the POST method. Try changing Angular to send an explicit POST or change routes.php so auth/signup responds to both GET and POST requests.
Update looking at your screen shots, the AJAX request is returning an error 500. There should be information logged to either your laravel.log file or your PHP/webserver error log as to why the error is happening. My guess if your Angular request sends different information that your Chrome/REST-app does, and that triggers a code path where there's an error.
Fixed the problem, turns my controller was calling an undefined method in the controller class.
Renamed the method correctly and the request now works, thanks guys for the input.