Composable for checking Laravel Sanctum's CSRF token in Nuxt 3 - laravel-9

I'm working on a Nuxt 3 app with Laravel 9 as API with Sanctum and Fortify.
I wrote a composable to check the response to the Laravel Sanctum's CSRF token HTTP route ('/sanctum/csrf-cookie') and I'm trying to check if it's working by logging via console.log() the composable's state, but it always shows an empty Proxy received as error.
Please help, what am I doing wrong?
Here is what I've done so far:
The composable: auth.ts :
export function useCsrfToken() {
const baseURL = useRuntimeConfig().public.LARAVEL_BASE_URL
const options = {
baseURL,
credentials: 'include',
headers: {
Accept: 'application/json',
} as HeadersInit,
method: 'GET',
}
const state = ref({
status: '',
error: {},
})
async function getCsrfToken() {
state.value.error = {}
state.value.status = ''
await $fetch('/sanctum/csrf-cookie', options)
.then(() => (state.value.status = 'ok'))
.catch((error) => {
console.log('error from useCsrfToken', error)
state.value.error = { error }
})
}
getCsrfToken()
return { state }
}
The login Nuxt page, where I'm using the above composable :
<script setup>
definePageMeta({
layout: false,
})
const submit = () => {
form.processing = true
const { state } = useCsrfToken()
console.log('error from login', state.value.error)
// console.log('status from login', status)
}
const form = reactive({
email: '',
password: '',
remember: false,
processing: false,
errors: {},
})
</script>
<template>
<div>
<!-- <TheTwoFactorChallenge v-if="verification" #2fapassed="submit" /> -->
<NuxtLayout name="auth">
<template #title>
<p class="card-header-title">Inicie Sesión</p>
</template>
<form novalidate #submit.prevent="submit">
<div class="field">
<label class="label" for="email">Usuario</label>
<div class="control has-icons-left has-icons-right">
<AppInput
type="email"
id="email"
v-model="form.email"
:error="form.errors?.email"
autocomplete="email"
required
/>
<AppIconLeft icon="fa-solid fa-envelope" />
<AppIconError v-if="form.errors?.email" class="is-right" />
</div>
<AppHelpError :errors="form.errors?.email" />
</div>
<div class="field">
<label class="label" for="password">Contraseña</label>
<div class="control has-icons-left has-icons-right">
<AppInput
type="password"
id="password"
v-model="form.password"
:error="form.errors?.password"
autocomplete="new-password"
required
/>
<AppIconLeft icon="fa-solid fa-lock" />
<AppIconError v-if="form.errors?.password" class="is-right" />
</div>
<AppHelpError :errors="form.errors?.password" />
</div>
<div class="field">
<div class="control">
<AppSwitch
id="remember"
v-model:checked="form.remember"
class="is-small is-link"
/>
<label for="remember">Recuérdame</label>
</div>
</div>
<div class="is-flex is-justify-content-flex-end mb-4">
<NuxtLink to="#" class="has-text-link">
¿Olvidó su contraseña?
</NuxtLink>
</div>
<div class="field">
<div class="control">
<AppButton
class="is-link is-fullwidth"
type="submit"
:is-loading="form.processing"
>Entrar</AppButton
>
</div>
</div>
</form>
</NuxtLayout>
</div>
</template>
And here are the outputs :

Since Nuxt3 provides a Server function which could use as an API, it could be easily confused with Laravel.
You may need to add a proxy_pass (if you use Nginx) to distinguish between Nuxt and Laravel requests in order to share the same origin between Nuxt and Laravel (to avoid wasting a second on Same-Origin-Policy )
For example:
http://example.com/ // -> nuxt
http://example.com/api/ // -> laravel
(Since you said you are using Docker, I'm going to assume you're using laravel sail)
First, add a domain (for example http://example.test) for your site instead of http://localhost.
Custom the /sanctum/csrf-cookie route to /api/sanctum/csrf-cookie (here you can see how to change laravel sanctum csrf cookie route)
Add a proxy_pass for http://example.test/api/* in Nginx configs:
location ~* ^/(api|broadcasting|storage)/ {
proxy_pass http://localhost:8080; // where your laravel running
}
But, in my view, the SSR Nuxt should be considered as a client just equal to an Android app or IOS app, because the frontend and the backend are entire two different projects compared with the previous time when putting your frontend code and backend code in the same project.
So, you may auth the web just like auth the Android app, instead of booting the CRSF protection.
And here is a full example of Nuxt3 + Laravel, which is using Laravel Sanctum and also SSR with authorizations. 👉 Laravel + SSR Nuxt3 with authorizations

Related

testing api sending OPTIONS method and getting 405 error [duplicate]

This question already has an answer here:
FastAPI's RedirectResponse doesn't work as expected in Swagger UI
(1 answer)
Closed 5 months ago.
Im new to vue and nuxt, im using this ones to connect them to my API made with fastapi, and whenever i try to create an account via my API with the vue form i get this error
127.0.0.1:52137 - "OPTIONS /user HTTP/1.1" 405 Method Not Allowed
I've seen that sometimes axios sends an OPTION method to "test" the api if i get it.. But how do i solve this problem ?
Im new to this so do not hesitate to ask me more files/code.
Here is the Post method that im trying to reach and my registration page on VUE.
#app.post("/user", response_model=_schemas.UserBis)
async def create_user(user: _schemas.UserIn, db: _orm.Session = fastapi.Depends(_services.get_db)):
db_user_email = await _services.get_user_by_email(email=user.email, db=db)
if db_user_email:
raise fastapi.HTTPException(
status_code=400, detail="User with that email already exists"
)
db_user_username = await _services.get_user_by_username(username=user.username, db=db)
if db_user_username:
raise fastapi.HTTPException(
status_code=400, detail="User with that email already exists"
)
db_user_pseudo = await _services.get_user_by_pseudo(pseudo=user.pseudo, db=db)
if db_user_pseudo:
raise fastapi.HTTPException(
status_code=400, detail="User with that pseudo already exists"
)
user = await _services.create_user(user=user, db=db)
return _schemas.UserBis(data=user)
VUE:
<template>
<section class="section">
<div class="container">
<div class="columns">
<div class="column is-4 is-offset-4">
<h2 class="title has-text-centered">Register!</h2>
<Notification :message="error" v-if="error"/>
<form method="post" #submit.prevent="register">
<div class="field">
<label class="label">Username</label>
<div class="control">
<input
type="text"
class="input"
name="username"
v-model="username"
required
/>
</div>
</div>
<div class="field">
<label class="label">Pseudo</label>
<div class="control">
<input
type="text"
class="input"
name="pseudo"
v-model="pseudo"
required
/>
</div>
</div>
<div class="field">
<label class="label">Email</label>
<div class="control">
<input
type="email"
class="input"
name="email"
v-model="email"
required
/>
</div>
</div>
<div class="field">
<label class="label">Password</label>
<div class="control">
<input
type="password"
class="input"
name="password"
v-model="password"
required
/>
</div>
</div>
<div class="control">
<button type="submit" class="button is-dark is-fullwidth">Register</button>
</div>
</form>
<div class="has-text-centered" style="margin-top: 20px">
Already got an account? <nuxt-link to="/login">Login</nuxt-link>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import Notification from '~/components/Notification'
export default {
components: {
Notification,
},
data() {
return {
username: '',
pseudo: '',
email: '',
password: '',
error: null
}
},
methods: {
async register() {
try {
await this.$axios.post('user', {
username: this.username,
pseudo: this.pseudo,
email: this.email,
password: this.password
})
await this.$auth.loginWith('local', {
data: {
email: this.email,
password: this.password
},
})
this.$router.push('/')
} catch (e) {
this.error = e.response.data.message
}
}
}
}
</script>
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
Any non simple POST request triggers a pre flight check (OPTIONS request) to confirm the action is supported. Your API will need to be modified to allow these requests.
I found a slution to my problem here: https://stackoverflow.com/a/66460861/18428648
Allowing all origins solved my problem, just added a few lines of code to my FastAPI API.

How can I Upload files and JSON data in the same request with AlpineJS

I need to Add data & image file in a same post request using alpine js.
I think it is a little bit late. But, someone else might need take benefit from this answer in the future.
You can use Javascript's FormData to handle both data and file.
Bellow, I am copying a part from my application to show the full process of actually uploading image with Alpine JS and sending it as part of Form Data to your API end.
HTML Part:
<form method="post" enctype="multipart/form-data" #submit.prevent="$store.app.submitData()">
<div class="row mb-4">
<div class="form-group col-md-4">
<label>App Name</label>
<input type="text" name="name" class="form-control" x-model="$store.app.form.name">
</div>
<div class="form-group col-md-4">
<label>Slug</label>
<input type="text" name="name" class="form-control" x-model="$store.app.form.slug">
</div>
<div class="form-group col-md-4">
<label>Icon/Logo</label>
<input type="file" name="image_icon" class="form-control" x-on:change="$store.app.selectFile($event)" accept="image/png, image/jpg, image/jpeg">
</div>
</div>
<div class="row">
<div class="col-md-12 text-end mt-3">
<button type="submit" class="btn btn-lg btn-primary mb-5" :disabled="$store.app.loading">
<span class="indicator-label">Save</span>
</button>
</div>
</div>
</form>
Alpine JS Part:
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('app', {
loading: false,
form: {
name: '',
image_icon: '',
slug: ''
},
selectFile(event) {
this.form.image_icon = event.target.files[0]
},
submitData() {
//Create an instance of FormData
const data = new FormData()
let url = '/application'
// Append the form object data by mapping through them
Object.keys(this.form).map((key, index) => {
data.append(key, this. Form[key])
});
this.loading = true
fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
body: data
})
.then(response => {
//...
})
.finally(() => {
this. Loading = false
});
}
})
})
</script>
Please note that I have used store in this example, you can use Alpine Data, function or inline x-data as you please.
Appending to the form data just requires a key and value pair, for example;
const data = new FormData();
data.append('name', 'John');
data.append('surname', 'Doe');
I hope this helps.

How can I create role based testing in Cypress?

I have a project that needs to be tested via Cypress. I am new in "Cypressing" by the way..
I have problem to find a solution about role-based testing where something like this happen:
If the xhr response gave a user data with role admin, it will redirect to dashboard/admin. Otherwise, if the xhr response gave a user data a role user, it will redirect to dashboard/user.
After that, each views may have different actions & behavior regarding to what kind of user who has logged in.
Please have a look at my script:
const BASE_URL = Cypress.env('BASE_URL')
const APP_NAME = Cypress.env('APP_NAME')
const AUTH_ID = Cypress.env('AUTH_ID')
const PASSWORD = Cypress.env('PASSWORD')
describe('Login Test', () => {
it('can perform a login action', () => {
cy.visit(BASE_URL)
cy.contains(APP_NAME)
cy.get('input[name="contact"]')
.type(AUTH_ID)
.should('have.value', AUTH_ID)
cy.get('input[name="password"]')
.type(PASSWORD)
.should('have.value', PASSWORD)
cy.get('#submit-button').click()
cy.url().should('contain', 'dashboard')
})
})
and please have a look at my Vue.js script too:
<template>
<div id="login">
<div class="login-card p-4">
<h1 class="text-center font-weight-bold text-primary py-5">STTKD</h1>
<!-- phone number input -->
<div class="form-group">
<label for="contact">Nomor Telepon</label>
<input
name="contact"
type="number"
class="form-control"
min="0"
placeholder="08xxx atau 628xxxx"
v-model="user.contact"
/>
</div>
<!-- password-input -->
<div class="form-group">
<label for="password">Password</label>
<input
name="password"
type="password"
class="form-control"
placeholder="Masukkan password"
v-model="user.password"
/>
</div>
<div class="form-group text-right">
Forgot password?
</div>
<!-- login-button -->
<div class="form-group">
<button
id="submit-button"
type="button"
class="btn btn-primary btn-block"
#click="onSubmit"
>LOGIN</button>
</div>
<div class="form-group text-center">
<p class="mb-0">
Don't have account?
<router-link :to="{ name: 'register' }" class="text-primary">Create a new one!</router-link>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
user: {
contact: "",
password: ""
}
};
},
methods: {
onSubmit() {
axios
.post("auth/login", this.user)
.then(resp => {
VueCookies.set("access_token", resp.data.access_token);
VueCookies.set("token_type", resp.data.token_type);
VueCookies.set("expires_at", resp.data.expires_at);
// redirect with" user" param if the roles array from the response includes "user"
if (resp.data.user.roles.includes("user")) {
this.$router.replace({
name: "dashboard",
params: {
role: "user"
}
});
return;
}
// else, redirect with "admin" param
this.$router.replace({
name: "dashboard",
params: {
role: "admin"
}
});
})
.catch(err => console.error(err));
}
}
};
</script>
As you see above, its running without problem but I have no idea about what to do next because the user behavior is kindof dynamic.
Please help me to solve this problem, any references are good for me if there is.
Thanks in advance.

How to login/logout using full stack Vue frontend with Laravel backend

I have been struggling night after night trying to implement a complete Vue front-end using a Laravel back-end on the same server. I can get normal resource requests done with axios just fine; however, loggin in and out is a bit of a nightmare! I have exhausted my ideas and really need some help :)
Here is my Login Vue component:
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Login</div>
<div class="card-body">
<form>
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">E-Mail Address</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control" name="email" required autocomplete="email" autofocus v-model="email">
<!-- <span class="invalid-feedback" role="alert">
<strong>message</strong>
</span> -->
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">Password</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control" name="password" required autocomplete="current-password" v-model="password">
<!-- <span class="invalid-feedback" role="alert">
<strong>message</strong>
</span> -->
</div>
</div>
<div class="form-group row">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember" v-model="remember">
<label class="form-check-label" for="remember">
Remember Me
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-8 offset-md-4">
<button #click.prevent="login" class="btn btn-primary">
Login
</button>
<a class="btn btn-link" href="">
Forgot Your Password?
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
email: null,
password: null,
remember: null,
loading: false
}
},
methods: {
login() {
window.axios.post('login', {
email: this.email, password: this.password
})
.then(response => {
console.log('Login fired!')
console.log(response.data)
})
.catch(error => {
console.log(error)
})
},
},
}
</script>
Here is my axios config in my 'bootstrap' file which I pull into my app.js:
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.baseURL = 'http://localhost:8000/api/';
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
Here is my app.js:
import './bootstrap'
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from './store/store'
import routes from './routes'
import App from './App.vue'
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
routes
})
const app = new Vue({
el: '#app',
router,
store,
render: h => h(App)
});
Now on the back-end side, I have pulled in my 'authenticated' and 'loggedOut' methods into my Login controller and return some json:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
/**
* The user has been authenticated.
*
* #param \Illuminate\Http\Request $request
* #param mixed $user
* #return mixed
*/
protected function authenticated(Request $request, $user)
{
return response()->json('You have successfully logged in', 200);
}
/**
* The user has logged out of the application.
*
* #param \Illuminate\Http\Request $request
* #return mixed
*/
protected function loggedOut(Request $request)
{
return response()->json('You have logged out now.', 200);
}
}
The problem I am getting when I try to login is the following console.log:
dispatchXhrRequest # app.js:279
xhrAdapter # app.js:118
dispatchRequest # app.js:726
Promise.then (async)
request # app.js:528
Axios.<computed> # app.js:553
wrap # app.js:1071
logout # app.js:2057
invokeWithErrorHandling # app.js:42933
invoker # app.js:43258
original._wrapper # app.js:48611
app.js:2060 Error: Request failed with status code 500
at createError (app.js:653)
at settle (app.js:899)
at XMLHttpRequest.handleLoad (app.js:166)
app.js:279 XHR failed loading: POST "http://localhost:8000/api/logout".
I get that response only some o the time, the rest of the time I get my only view returned as the response from the axios call.
Here are my Laravel web routes:
// Authentication Routes...
Route::post('api/login', 'Auth\LoginController#login');
Route::post('api/logout', 'Auth\LoginController#logout')->name('logout');
// Registration Routes...
Route::post('register', 'Auth\RegisterController#register');
Route::get('{any?}', function () {
return view('index');
// });
// })->where('any', '^(?!api).*$');
})->where('any', '.*');
And here is my Vue router:
{ path: '/login', component: LoginPage },
{ path: '/register', component: RegisterPage },
First add in your form
then submit button to submit and call login method in vuejs method section.In my case
Login
In vuejs component method i put
document.getElementById("login_form").submit();
If you are using Laravel 6, then there will be Auth::routes() in web.php under routes.php. Login and logout request from your will be handled trhough Auth::routes(). There is no need to add routes to vuejs router and handling these requests in api.php.The logout method is called in same way.

GO button does nothing (AIML rails 5)

I have installed programr. I followed tutorial from this site http://dreamingechoes.github.io/bot/ruby/rails/conversational-bot-ruby-on-rails/
bot.rb
require 'programr'
brains = Dir.glob("lib/bot/*")
BOT = ProgramR::Facade.new
BOT.learn(brains)
application_controller.rb
def ask_bot
reaction = BOT.get_reaction(params[:query])
render json: { response: reaction.present? ? reaction : "I don't have an answer to that" }
end
bot.aiml
<?xml version="1.0" encoding="UTF-8"?>
<aiml version="1.0" xmlns="http://alicebot.org/2001/AIML-1.0.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://alicebot.org/2001/AIML-1.0.1 http://aitools.org/aiml/schema/AIML.xsd">
<category>
<pattern>Hello *</pattern>
<template>
Hey, how can I help you?
</template>
</category>
<category>
<pattern>*bye</pattern>
<template>
Always here for you!
</template>
</category>
<category>
<pattern>What payment methods do you accept?</pattern>
<template>
We accept Visa, MasterCard and American Express.
</template>
</category>
</aiml>
view:
<div class="alert alert-info">
×
<strong>Info!</strong> Type something on the text input and hit the <strong>GO</strong> button to get a response.
</div>
<div class="bs-callout bs-callout-info bot-response hide" id="callout-alerts-no-default">
<h4>Bot says:</h4>
<p id="bot-response"> </p>
</div>
<div class="row home-row">
<div class="col-lg-12">
<div class="input-group">
<input id="query" type="text" class="form-control" placeholder="Say something to the bot...">
<span class="input-group-btn">
<button id="ask" class="btn btn-default" type="button">GO</button>
</span>
</div>
</div>
</div>
application.js
$(document).ready(function(){
$('#ask').on('click', function(event) {
$.ajax({
url: '/ask_bot',
type: 'json',
method: 'get',
data: { query: $('#query').val() },
success: function(data) {
$('.bot-response').removeClass('hide');
$('#bot-response').html(data['response']);
$('#query').val('');
}
});
});
});
If i copy a pattern and paste in the form, the GO button does nothing. Please help...thanks!
Most probably what you'll have to use is the DOMContentLoaded in order to check when the DOM has finished loading, this way you can wrap the listener for the #ask button and the AJAX function, without having to modify your "requires" in the application.js file, nor to have to include them as script tags in your views.
You could try with something like:
document.addEventListener('DOMContentLoaded', function () {
...
});
And inside to put your JS code, which would remain as:
document.addEventListener('DOMContentLoaded', function () {
$('#ask').on('click', function(event) {
$.ajax({
url: '/ask_bot',
type: 'json',
method: 'get',
data: { query: $('#query').val() },
success: function(data) {
$('.bot-response').removeClass('hide');
$('#bot-response').html(data['response']);
$('#query').val('');
}
});
});
$('[data-toggle="tooltip"]').tooltip();
});
Also you could add the specific custom.js for the specific controller and action in which is being used, that's the reason of the Uncaught TypeError: Cannot read property 'getContext' of null error, because such elements barChart and barChart2 are in the back_office#index.