feat(Login): add example jwt token
This commit is contained in:
42
src/components/Login.vue
Normal file
42
src/components/Login.vue
Normal 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>
|
||||
7
src/pages/autenticarse.vue
Normal file
7
src/pages/autenticarse.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<Login />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
7
src/pages/login.vue
Normal file
7
src/pages/login.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<Login />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
49
src/services/auth.js
Normal file
49
src/services/auth.js
Normal 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;
|
||||
@@ -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 re‑intentar
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user