Implementando autenticación usando jwt #28 #31

Merged
mono merged 12 commits from implement_jwt_authentication_#28 into main 2026-03-07 17:30:23 -05:00
6 changed files with 160 additions and 34 deletions
Showing only changes of commit 173ddfd05f - Show all commits

42
src/components/Login.vue Normal file
View File

@@ -0,0 +1,42 @@
<script setup>
import { ref } from 'vue';
import AuthService from '@/services/auth';
import { inject } from 'vue';
const username = ref('');
const password = ref('');
const error = ref('');
async function login() {
try {
await AuthService.login({ username: username.value, password: password.value });
// opcional: redirigir al dashboard
} catch (e) {
error.value = e.message;
}
}
// ejemplo de llamada a clientes (requiere token)
const api = inject('api');
async function loadCustomers() {
try {
const data = await api.getCustomers();
console.log(data);
} catch (e) {
console.error(e);
}
}
</script>
<template>
<h1>Login</h1>
<v-form @submit.prevent="login">
<v-text-field v-model="username" label="Usuario" required />
<v-text-field v-model="password" label="Contraseña" type="password" required />
<v-btn type="submit">Entrar</v-btn>
<v-alert v-if="error" type="error">{{ error }}</v-alert>
</v-form>
<v-btn @click="loadCustomers">Cargar clientes</v-btn>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<Login />
</template>
<script setup>
//
</script>

7
src/pages/login.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<Login />
</template>
<script setup>
//
</script>

49
src/services/auth.js Normal file
View File

@@ -0,0 +1,49 @@
class AuthService {
static TOKEN_KEY = 'access_token';
static REFRESH_KEY = 'refresh_token';
static login(credentials) {
const url = `${import.meta.env.VITE_DJANGO_BASE_URL}/api/token/`;
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
})
.then(r => r.json())
.then(data => {
localStorage.setItem(this.TOKEN_KEY, data.access);
localStorage.setItem(this.REFRESH_KEY, data.refresh);
return data;
});
}
static getAccessToken() {
return localStorage.getItem(this.TOKEN_KEY);
}
static getRefreshToken() {
return localStorage.getItem(this.REFRESH_KEY);
}
static async refresh() {
const refresh = this.getRefreshToken();
if (!refresh) throw new Error('No refresh token');
const url = `${import.meta.env.VITE_DJANGO_BASE_URL}/api/token/refresh/`;
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh }),
});
const data = await resp.json();
localStorage.setItem(this.TOKEN_KEY, data.access);
return data.access;
}
static logout() {
localStorage.removeItem(this.TOKEN_KEY);
localStorage.removeItem(this.REFRESH_KEY);
}
}
export default AuthService;

View File

@@ -1,8 +1,38 @@
import AuthService from '@/services/auth';
class DjangoApi {
constructor() {
this.base = import.meta.env.VITE_DJANGO_BASE_URL;
}
_authHeaders() {
const token = AuthService.getAccessToken();
return token ? { Authorization: `Bearer ${token}` } : {};
}
_handleResponse(response) {
if (!response.ok) {
// Si el token ha expirado (401) intentamos refrescar y reintentar
if (response.status === 401) {
return AuthService.refresh().then(newToken => {
// volver a ejecutar la petición original con el nuevo token
const retryHeaders = {
...response.headers,
Authorization: `Bearer ${newToken}`,
};
// aquí usamos fetch de nuevo con los mismos parámetros
// (para simplificar, delegamos a getRequest/postRequest)
// En la práctica, extrae la lógica a una función reutilizable.
throw new Error('Retry logic should be implemented here');
});
}
return response.json().then(err => {
throw new Error(`Error ${response.status}: ${err.detail || response.statusText}`);
});
}
return response.json();
}
getCustomers() {
const url = this.base + '/don_confiao/api/customers/';
return this.getRequest(url);
@@ -79,42 +109,23 @@ class DjangoApi {
}
getRequest(url) {
return new Promise ((resolve, reject) => {
fetch(url)
.then(response => response.json())
.then(data => {
resolve(data);
})
.catch(error => {
reject(error);
});
});
return fetch(url, {
headers: {
'Content-Type': 'application/json',
...this._authHeaders(),
},
}).then(this._handleResponse);
}
postRequest(url, content) {
return new Promise((resolve, reject) => {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(content)
})
.then(response => {
if (!response.ok) {
reject(new Error(`Error ${response.status}: ${response.statusText}`));
} else {
response.json().then(data => {
if (!data) {
reject(new Error('La respuesta no es un JSON válido'));
} else {
resolve(data);
}
});
}
})
.catch(error => reject(error));
});
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this._authHeaders(),
},
body: JSON.stringify(content),
}).then(this._handleResponse);
}
}

View File

@@ -11,6 +11,16 @@ import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
const aliasMap = {
'@': fileURLToPath(new URL('./src', import.meta.url)),
};
console.log('🔧 Alias configurado:');
console.log(aliasMap); // <-- se muestra en la terminal al iniciar Vite
console.log('Resolución real de "@":', aliasMap['@']);
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
@@ -50,7 +60,7 @@ export default defineConfig({
} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
extensions: [
'.js',