diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..76df82e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+VITE_API_IMPLEMENTATION=django
+VITE_DJANGO_BASE_URL=http://localhost:7000
diff --git a/.gitignore b/.gitignore
index da3df75..9ca2b60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ node_modules
# local env files
.env.local
.env.*.local
+.env
# Log files
npm-debug.log*
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..78fb193
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,81 @@
+# Don Confiao - Frontend
+
+## Tech Stack
+- **Framework:** Vue 3 (Composition API)
+- **UI Library:** Vuetify 3
+- **Routing:** Vue Router 4 (auto-routes con `unplugin-vue-router`)
+- **State:** Pinia
+- **HTTP:** Axios
+- **Build:** Vite
+- **Linting:** ESLint
+
+## Project Structure
+```
+src/
+├── assets/ # Imágenes, iconos estáticos
+├── components/ # Componentes Vue reutilizables
+├── layouts/ # Layouts de página
+├── pages/ # Vistas (auto-routed desde文件名)
+├── plugins/ # Configuración de Vuetify, etc.
+├── router/ # Configuración de rutas
+├── services/ # API services (auth.js, etc.)
+├── stores/ # Pinia stores
+└── styles/ # SCSS settings
+```
+
+## Important Conventions
+
+### Auto-imports
+- Componentes en `src/components/` se auto-importan por nombre
+- Los archivos en `src/pages/*.vue` se routing automáticamente via `unplugin-vue-router`
+- Alias `@` = `src/`
+
+### Pages (CRITICAL)
+**Siempre importar componentes en los archivos de página:**
+```vue
+
+
+
+
+
+```
+
+### Componentes
+- Usar Composition API (`
diff --git a/src/components/LoginDialog.vue b/src/components/LoginDialog.vue
new file mode 100644
index 0000000..df79108
--- /dev/null
+++ b/src/components/LoginDialog.vue
@@ -0,0 +1,69 @@
+
+
+
+ Iniciar sesión
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ Cancelar
+ Entrar
+
+
+
+
+
+
diff --git a/src/components/Logout.vue b/src/components/Logout.vue
new file mode 100644
index 0000000..7e1098e
--- /dev/null
+++ b/src/components/Logout.vue
@@ -0,0 +1,32 @@
+
+
+
+ Cerrando sesión…
+
+
+
+
+
+
diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue
index 4a4be41..0f2f5ef 100644
--- a/src/components/NavBar.vue
+++ b/src/components/NavBar.vue
@@ -3,11 +3,22 @@
Menu
-
-
-
-
-
+
+ Login
+
+
+ Logout
+
import trytonIcon from '../assets/icons/tryton-icon.svg';
+ import AuthService from '@/services/auth';
export default {
name: 'NavBar',
data: () => ({
drawer: false,
group: null,
showAdminMenu: false,
+ isAuthenticated: false,
menuItems: [
{ title: 'Inicio', route: '/', icon: 'mdi-home'},
{ title: 'Comprar', route:'/comprar', icon: 'mdi-cart'},
@@ -61,12 +74,21 @@
{ title: 'Actualizar Ventas Tryton', route: '/sincronizar_ventas_tryton', icon: 'trytonIcon'}
],
}),
+ mounted() {
+ this.checkAuth();
+ },
watch: {
group () {
this.drawer = false
},
+ $route() {
+ this.checkAuth();
+ },
},
methods: {
+ checkAuth() {
+ this.isAuthenticated = AuthService.isAuthenticated();
+ },
navigate(route) {
this.$router.push(route);
},
@@ -77,6 +99,11 @@
toggleAdminMenu() {
this.showAdminMenu = !this.showAdminMenu;
},
+ logout() {
+ AuthService.logout();
+ this.isAuthenticated = false;
+ this.$router.push('/');
+ },
}
}
-
+
diff --git a/src/pages/autenticarse.vue b/src/pages/autenticarse.vue
new file mode 100644
index 0000000..d6a52a2
--- /dev/null
+++ b/src/pages/autenticarse.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/pages/salir.vue b/src/pages/salir.vue
new file mode 100644
index 0000000..72751f5
--- /dev/null
+++ b/src/pages/salir.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/services/auth.js b/src/services/auth.js
new file mode 100644
index 0000000..87743b6
--- /dev/null
+++ b/src/services/auth.js
@@ -0,0 +1,72 @@
+class AuthService {
+ static TOKEN_KEY = 'access_token';
+ static REFRESH_KEY = 'refresh_token';
+
+ static async login(credentials) {
+ const url = `${import.meta.env.VITE_DJANGO_BASE_URL}/api/token/`;
+
+ const resp = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(credentials),
+ });
+
+ if (!resp.ok) {
+ let errMsg = resp.statusText;
+ try {
+ const errData = await resp.json();
+ errMsg = errData?.detail ?? errData?.message ?? errMsg;
+ } catch (_) { /* ignore */ }
+ throw new Error(errMsg);
+ }
+
+ const data = await resp.json();
+
+ if (data.access && data.refresh) {
+ 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 }),
+ });
+
+ if (!resp.ok) {
+ const errData = await resp.json().catch(() => ({}));
+ throw new Error(errData?.detail ?? resp.statusText);
+ }
+
+ const data = await resp.json();
+ localStorage.setItem(this.TOKEN_KEY, data.access);
+ return data.access;
+ }
+
+ static isAuthenticated() {
+ return !!this.getAccessToken();
+ }
+
+ static logout() {
+ localStorage.removeItem(this.TOKEN_KEY);
+ localStorage.removeItem(this.REFRESH_KEY);
+ }
+}
+
+export default AuthService;
diff --git a/src/services/django-api.js b/src/services/django-api.js
index 653ad3f..b302065 100644
--- a/src/services/django-api.js
+++ b/src/services/django-api.js
@@ -1,8 +1,19 @@
+import AuthService from '@/services/auth';
+import http from '@/services/http';
+
class DjangoApi {
constructor() {
this.base = import.meta.env.VITE_DJANGO_BASE_URL;
}
+ getRequest(url) {
+ return http.get(url).then(r => r.data);
+ }
+
+ postRequest(url, payload) {
+ return http.post(url, payload).then(r => r.data);
+ }
+
getCustomers() {
const url = this.base + '/don_confiao/api/customers/';
return this.getRequest(url);
@@ -77,45 +88,6 @@ class DjangoApi {
const url = this.base + '/don_confiao/api/enviar_ventas_a_tryton';
return this.postRequest(url, {});
}
-
- getRequest(url) {
- return new Promise ((resolve, reject) => {
- fetch(url)
- .then(response => response.json())
- .then(data => {
- resolve(data);
- })
- .catch(error => {
- reject(error);
- });
- });
- }
-
- 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));
- });
- }
}
- export default DjangoApi;
+export default DjangoApi;
diff --git a/src/services/http.js b/src/services/http.js
new file mode 100644
index 0000000..c00f04b
--- /dev/null
+++ b/src/services/http.js
@@ -0,0 +1,44 @@
+import axios from 'axios';
+import AuthService from '@/services/auth';
+
+const http = axios.create({
+ baseURL: import.meta.env.VITE_DJANGO_BASE_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+http.interceptors.request.use(
+ config => {
+ const token = AuthService.getAccessToken();
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ error => Promise.reject(error)
+);
+
+http.interceptors.response.use(
+ response => response,
+ async error => {
+ const originalRequest = error.config;
+
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ originalRequest._retry = true;
+ try {
+ const newAccess = await AuthService.refresh();
+ originalRequest.headers.Authorization = `Bearer ${newAccess}`;
+ return http.request(originalRequest);
+ } catch (refreshError) {
+ AuthService.logout();
+ window.location.href = '/autenticarse';
+ return Promise.reject(refreshError);
+ }
+ }
+
+ return Promise.reject(error);
+ }
+);
+
+export default http;
diff --git a/vite.config.mjs b/vite.config.mjs
index a1a9dec..bfb192d 100644
--- a/vite.config.mjs
+++ b/vite.config.mjs
@@ -50,7 +50,7 @@ export default defineConfig({
} },
resolve: {
alias: {
- '@': fileURLToPath(new URL('./src', import.meta.url))
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
},
extensions: [
'.js',