Criar um sistema de autenticação seguro e funcional é uma das tarefas mais fundamentais em qualquer aplicação moderna. Quando combinamos o poder do Laravel no backend com a flexibilidade do Vue.JS e o design refinado do Quasar Framework no frontend, conseguimos construir uma aplicação robusta, segura e com excelente experiência de usuário.
Neste artigo, vamos explorar a arquitetura de um sistema completo de login com redefinição de senha, já implementado e funcional, destacando a organização dos arquivos, a separação de responsabilidades e como cada parte se comunica com a outra. Este projeto segue boas práticas de desenvolvimento, segurança e estrutura moderna, ideal para quem deseja evoluir além dos tutoriais básicos.
Tabela de Conteúdo
Organização do projeto
Backend — Laravel API
No backend, utilizamos o Laravel como API REST, garantindo alta produtividade e segurança com recursos prontos para autenticação.
A estrutura principal está organizada da seguinte forma:
app/
├── Http/
│ └── Controllers/
│ └── Auth/
│ ├── ForgotPasswordController.php
│ ├── LoginController.php
│ ├── RegisterController.php
│ └── ResetPasswordController.php
├── Models/
│ └── User.php
├── Notifications/
│ └── ResetPasswordNotification.php
routes/
└── api.php
.env
Cada controller tem uma responsabilidade clara:
LoginController.php: Gerencia autenticação, incluindo geração de token.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\HasApiTokens;
class LoginController extends Controller
{
public function login(Request $request)
{
// Valida as credenciais do usuário
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
// Tenta autenticar o usuário com as credenciais fornecidas. Se falhar, retorna um erro 401.
if (!Auth::attempt($credentials)) {
return response()->json(['message' => 'Credenciais inválidas'], 401);
}
// Se a autenticação for bem-sucedida, gera um token de acesso para o usuário autenticado.
$user = Auth::user();
$token = $user->createToken('auth_token')->plainTextToken;
// Retorna o token de acesso e os dados do usuário em formato JSON. Os dados do usuário incluem o ID, nome e email, mas não a senha por motivos de segurança.
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'user' => $user,
]);
}
public function logout(Request $request)
{
// Remove o token de acesso atual do usuário autenticado
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logout realizado com sucesso'
]);
}
}
RegisterController.php: Registra novos usuários com validação e hashing de senha.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
/**
* Controlador para registro de usuários.
*/
class RegisterController extends Controller
{
// Método responsável por registrar um novo usuário. Ele recebe uma requisição HTTP contendo os dados do usuário, valida esses dados e cria um novo registro no banco de dados.
public function register(Request $request)
{
// Valida os dados da requisição. O método validate verifica se os campos 'name', 'email' e 'password' estão presentes e atendem aos critérios especificados:
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
// Cria um novo usuário com os dados fornecidos na requisição. O campo 'password' é criptografado usando o Hash::make para garantir a segurança da senha.
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Após criar o usuário, gera um token de acesso para ele. Isso permite que o usuário faça requisições autenticadas posteriormente.
return response()->json(['message' => 'Usuário registrado com sucesso!']);
}
}
ForgotPasswordController.php: Envia e-mails com tokens para redefinição de senha.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ForgotPasswordController extends Controller
{
public function sendResetLinkEmail(Request $request)
{
$request->validate(['email' => 'required|email']);
$status = Password::sendResetLink(
$request->only('email')
);
return $status === Password::RESET_LINK_SENT
? response()->json(['message' => __($status)])
: response()->json(['message' => __($status)], 400);
}
}
ResetPasswordController.php: Valida o token e atualiza a senha.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Hash;
class ResetPasswordController extends Controller
{
public function reset(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:8',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->password = Hash::make($password);
$user->save();
}
);
return $status === Password::PASSWORD_RESET
? response()->json(['message' => __($status)])
: response()->json(['message' => __($status)], 400);
}
}
ResetPasswordNotification.php: Personaliza o conteúdo do e-mail de redefinição, centralizando a lógica de notificação.
<?php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ResetPasswordNotification extends Notification
{
public $token;
public function __construct($token)
{
$this->token = $token;
}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
$url = 'http://localhost:9000/reset-password?token=' . $this->token . '&email=' . urlencode($notifiable->email);
return (new MailMessage)
->subject('Redefinição de senha')
->line('Você está recebendo este e-mail porque solicitou a redefinição de senha.')
->action('Redefinir senha', $url)
->line('Se você não solicitou, ignore este e-mail.');
}
}
O arquivo api.php
registra todas as rotas públicas e protegidas via middleware, mantendo o sistema coeso e seguro.
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\ResetPasswordController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Aqui você pode registrar as rotas da sua API. Essas rotas são carregadas
| pelo RouteServiceProvider e todas elas estarão no grupo de middleware "api".
|
*/
// Rotas públicas de autenticação
Route::prefix('auth')->group(function () {
Route::post('/register', [RegisterController::class, 'register']);
Route::post('/login', [LoginController::class, 'login']);
Route::post('/forgot-password', [ForgotPasswordController::class, 'sendResetLinkEmail']);
Route::post('/reset-password', [ResetPasswordController::class, 'reset']);
});
// Rotas protegidas por autenticação Sanctum
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
Route::post('/logout', [LoginController::class, 'logout']);
});
No .env
colocamos as crendicias SMTP para enviar os e-mail, você pode testar com serviços gratuitos como o Mailtrap.
# MAIL_MAILER=smtp
# MAIL_HOST=sandbox.smtp.mailtrap.io
# MAIL_PORT=2525
# MAIL_USERNAME=1b950d513g105b
# MAIL_PASSWORD=90b1192715135a
Frontend — Vue.js + Quasar
No frontend, adotamos uma SPA (Single Page Application) construída com Vue 3 e o framework Quasar, focado em responsividade e experiência de usuário moderna.
Estrutura dos arquivos principais:
pages/
├── DashboardPage.vue
├── ErrorNotFound.vue
├── ForgotPassword.vue
├── IndexPage.vue
├── ResetPassword.vue
└── UserRegister.vue
router/
├── index.js
└── routes.js
composables/
└── useAuth.js
As páginas estão bem separadas conforme os fluxos de autenticação:
IndexPage.vue: Tela inicial com rotas públicas, login.

<template>
<q-page class="flex flex-center">
<q-card style="width: 400px;">
<q-card-section>
<div class="text-h6">Login</div>
</q-card-section>
<q-card-section>
<q-form @submit="onSubmit" class="q-gutter-md">
<q-input filled v-model="email" label="E-mail" type="email" required />
<q-input filled v-model="password" label="Senha" type="password" required />
<q-btn label="Entrar" type="submit" color="primary" />
</q-form>
<div class="q-mt-md">
<q-btn flat label="Registrar" @click="goRegister" />
<q-btn flat label="Esqueci minha senha" @click="goForgot" />
</div>
<div v-if="error" class="q-mt-md text-negative">{{ error }}</div>
</q-card-section>
</q-card>
</q-page>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const email = ref('')
const password = ref('')
const error = ref('')
const router = useRouter()
// Função para lidar com o envio do formulário e autenticação do usuário.
const onSubmit = async () => {
// Limpa a mensagem de erro antes de tentar fazer login
error.value = ''
try {
// Envia uma requisição POST para a API de login
const response = await axios.post('http://localhost:8000/api/auth/login', {
email: email.value,
password: password.value
})
// Salve o token se quiser autenticação persistente
localStorage.setItem('token', response.data.access_token)
// Redirecione para o dashboard ou página protegida
router.push('/dashboard')
} catch (err) {
error.value = err.response?.data?.message || 'Erro ao fazer login'
}
}
const goRegister = () => router.push('/register')
const goForgot = () => router.push('/forgot-password')
</script>
UserRegister.vue: Tela de cadastro com validações visuais e conexão com o backend.

<template>
<q-page class="flex flex-center">
<q-card style="width: 400px;">
<q-card-section>
<div class="text-h6">Registrar</div>
</q-card-section>
<q-card-section>
<q-form @submit="onSubmit" class="q-gutter-md">
<q-input filled v-model="name" label="Nome" required />
<q-input filled v-model="email" label="E-mail" type="email" required />
<q-input filled v-model="password" label="Senha" type="password" required />
<q-input filled v-model="password_confirmation" label="Confirme a senha" type="password" required />
<q-btn label="Registrar" type="submit" color="primary" />
<q-btn flat label="Voltar para o login" color="primary" class="q-mt-md" @click="goLogin" />
</q-form>
<div v-if="message" class="q-mt-md text-positive">{{ message }}</div>
<div v-if="error" class="q-mt-md text-negative">{{ error }}</div>
</q-card-section>
</q-card>
</q-page>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
const name = ref('')
const email = ref('')
const password = ref('')
const password_confirmation = ref('')
const message = ref('')
const error = ref('')
const router = useRouter()
const onSubmit = async () => {
message.value = ''
error.value = ''
try {
await axios.post('http://localhost:8000/api/auth/register', {
name: name.value,
email: email.value,
password: password.value,
password_confirmation: password_confirmation.value
})
message.value = 'Usuário registrado com sucesso! Faça login.'
} catch (err) {
error.value = err.response?.data?.message || 'Erro ao registrar'
}
}
function goLogin() {
router.push('/')
}
</script>
DashboardPage.vue: Área protegida, acessada apenas por usuários autenticados.

<template>
<q-page class="flex flex-center">
<div class="column items-center">
<h2>Bem-vindo ao Dashboard!</h2>
<p>Você está logado.</p>
<q-btn label="Sair" color="negative" class="q-mt-md" @click="logout" />
</div>
</q-page>
</template>
<script setup>
import { useAuth } from 'src/composables/useAuth'
const { logout } = useAuth()
</script>
ForgotPassword.vue: Formulário para envio do e-mail de redefinição de senha.

<template>
<q-page class="flex flex-center">
<q-card style="width: 400px;">
<q-card-section>
<div class="text-h6">Esqueci minha senha</div>
</q-card-section>
<q-card-section>
<q-form @submit="onSubmit" class="q-gutter-md">
<q-input filled v-model="email" label="E-mail" type="email" required />
<q-btn label="Enviar link de redefinição" type="submit" color="primary" />
<q-btn flat label="Voltar para o login" color="primary" class="q-mt-md" @click="goLogin" />
</q-form>
<div v-if="message" class="q-mt-md text-positive">{{ message }}</div>
<div v-if="error" class="q-mt-md text-negative">{{ error }}</div>
</q-card-section>
</q-card>
</q-page>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
const email = ref('')
const message = ref('')
const error = ref('')
const router = useRouter()
const onSubmit = async () => {
message.value = ''
error.value = ''
try {
const response = await axios.post('http://localhost:8000/api/auth/forgot-password', { email: email.value })
message.value = response.data.message
} catch (err) {
error.value = err.response?.data?.message || 'Erro ao enviar e-mail'
}
}
function goLogin() {
router.push('/')
}
</script>
ResetPassword.vue: Tela que aceita o token enviado por e-mail e permite redefinir a senha.

<template>
<q-page class="flex flex-center">
<q-card style="width: 400px;">
<q-card-section>
<div class="text-h6">Redefinir senha</div>
</q-card-section>
<q-card-section>
<q-form @submit="onSubmit" class="q-gutter-md">
<!-- Campos email e token ficam escondidos -->
<input type="hidden" v-model="email" />
<input type="hidden" v-model="token" />
<q-input filled v-model="password" label="Nova senha" type="password" required />
<q-input filled v-model="password_confirmation" label="Confirme a nova senha" type="password" required />
<q-btn label="Redefinir senha" type="submit" color="primary" />
</q-form>
<div v-if="message" class="q-mt-md text-positive">{{ message }}</div>
<div v-if="error" class="q-mt-md text-negative">{{ error }}</div>
</q-card-section>
</q-card>
</q-page>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios from 'axios'
const route = useRoute()
const router = useRouter()
const email = ref('')
const token = ref('')
const password = ref('')
const password_confirmation = ref('')
const message = ref('')
const error = ref('')
onMounted(() => {
email.value = route.query.email || ''
token.value = route.query.token || ''
})
const onSubmit = async () => {
message.value = ''
error.value = ''
try {
const response = await axios.post('http://localhost:8000/api/auth/reset-password', {
email: email.value,
token: token.value,
password: password.value,
password_confirmation: password_confirmation.value
})
message.value = response.data.message
setTimeout(() => {
router.push('/')
}, 2000)
} catch (err) {
error.value = err.response?.data?.message || 'Erro ao redefinir senha'
}
}
</script>
Além disso, o composable useAuth.js
centraliza as funções de autenticação, facilitando o uso de login, logout, verificação de autenticação e manipulação de token JWT via localStorage.
import { useRouter } from 'vue-router'
import axios from 'axios'
export function useAuth() {
const router = useRouter()
const logout = async () => {
const token = localStorage.getItem('token')
if (token) {
try {
await axios.post(
'http://localhost:8000/api/logout',
{},
{ headers: { Authorization: `Bearer ${token}` } },
)
} catch (error) {
// Aqui você pode exibir um toast/snackbar se quiser
console.error('Erro ao fazer logout:', error)
}
}
localStorage.removeItem('token')
router.push('/')
}
return { logout }
}
Segurança e boas práticas
O projeto segue boas práticas tanto no backend quanto no frontend:
- Tokens JWT são usados para autenticação segura.
- CORS configurado corretamente no Laravel para permitir a comunicação com o frontend.
- Senhas com hash Bcrypt, protegidas por padrão no Laravel.
- Rotas protegidas com middleware (
auth:sanctum
). - Validações robustas tanto no frontend (com Quasar Rules) quanto no backend (Laravel Form Requests ou validação direta nos controllers).
- Arquitetura limpa e baseada em responsabilidades separadas, facilitando manutenção e testes.
Conclusão
Um sistema de login com redefinição de senha é essencial para qualquer aplicação que lide com dados sensíveis de usuários. Combinando Laravel, Vue.js e Quasar, é possível construir uma aplicação moderna, modular e segura, com UX de alto nível e código reutilizável.
Este projeto está estruturado para escalar, permitindo adições como autenticação com redes sociais, 2FA e permissões avançadas com pacotes como Laravel Permission.
Se você busca criar algo similar ou aprimorar seu sistema atual, adotar uma estrutura como esta pode acelerar muito seu desenvolvimento sem abrir mão de segurança ou qualidade.