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 @@ + + + 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 @@ + + + + + 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',