@robosophy/auth 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,141 +1,143 @@
1
- # @robosophy/auth
2
-
3
- Sesión de los fronts de Mi Cuenta contra el Landing: access token RS256 de 15 minutos en `localStorage["token"]`, refresh opaco en cookie HttpOnly y datos del usuario desde `GET /api/usuario/self`.
4
-
5
- No depende de ningún framework. Necesita `axios` (peer dependency) y `fetch`.
6
-
7
- ## Instalación
8
-
9
- ```sh
10
- npm install @robosophy/auth
11
- ```
12
-
13
- ## Configuración
14
-
15
- Se llama una sola vez, al arrancar la app:
16
-
17
- ```js
18
- import { configureAuth } from "@robosophy/auth";
19
-
20
- configureAuth({
21
- landingUrl: import.meta.env.VITE_LANDING_URL, // origin del Landing (front y back comparten origin)
22
- devEmail: import.meta.env.VITE_DEV_EMAIL, // solo DEV
23
- isDev: import.meta.env.DEV,
24
- });
25
- ```
26
-
27
- ## API
28
-
29
- | Función | Qué hace |
30
- |---|---|
31
- | `configureAuth(opts)` | Guarda `landingUrl`, `devEmail` e `isDev`. |
32
- | `getToken()` | Devuelve `localStorage["token"]` o `null`. |
33
- | `attachAuthRefresh(api)` | Agrega el Bearer a cada request y ante un 401 renueva el token y reintenta una vez. |
34
- | `getCurrentUser()` | `GET {landingUrl}/api/usuario/self`, cacheado en memoria. Si falla, el cache se descarta. |
35
- | `setSession(token)` | Guarda el token nuevo e invalida el cache del usuario. |
36
- | `logout()` | `POST {landingUrl}/api/auth/logout`, limpia la sesión y va a `{landingUrl}/login`. |
37
- | `clearSession()` | Borra `token`, `user-id`, `originalToken` y `token-landing` de `localStorage`. |
38
-
39
- ## Cómo se renueva el token
40
-
41
- Ante un 401 de un request que todavía no se reintentó:
42
-
43
- 1. Se toma el lock `basf-auth-refresh` con `navigator.locks`, compartido entre pestañas. Si el navegador no lo soporta, se deduplica solo dentro de la pestaña.
44
- 2. Si `localStorage["token"]` ya cambió, otra pestaña renovó primero y se usa ese token.
45
- 3. Si no, se pide uno nuevo:
46
- - En DEV con `devEmail`: `POST {landingUrl}/dev/token` con `{ email }`. El Landing back local lo expone solo en el perfil `local`.
47
- - Si no: `POST {landingUrl}/api/auth/refresh` con `credentials: "include"`, que rota la cookie de refresh.
48
- 4. Con token nuevo se reintenta el request original. Si no hay token:
49
- - En DEV: `console.warn("[auth] sin sesión: definí VITE_DEV_EMAIL en .env.local")` y el error sigue. No redirige.
50
- - En prod: `clearSession()` y redirect a `{landingUrl}/login`, salvo que ya esté en esa página.
51
-
52
- Los 401 de `/api/auth/login`, `/api/auth/verify-2fa`, `/api/auth/resend-2fa`, `/api/auth/refresh`, `/api/auth/logout` y `/dev/token` no disparan el refresh: ahí el 401 es la respuesta (credenciales, código 2FA, refresh vencido). El resto de `/api/auth/*`, como `request-2fa-challenge`, sí renueva y reintenta.
53
-
54
- ## Uso en Vue
55
-
56
- ```js
57
- // main.js
58
- import { configureAuth } from "@robosophy/auth";
59
-
60
- configureAuth({
61
- landingUrl: import.meta.env.VITE_LANDING_URL,
62
- devEmail: import.meta.env.VITE_DEV_EMAIL,
63
- isDev: import.meta.env.DEV,
64
- });
65
- ```
66
-
67
- ```js
68
- // axiosClient.js
69
- import axios from "axios";
70
- import { attachAuthRefresh } from "@robosophy/auth";
71
-
72
- const api = axios.create({ baseURL: import.meta.env.VITE_URL });
73
- attachAuthRefresh(api);
74
- export default api;
75
- ```
76
-
77
- ```js
78
- // router/index.js
79
- import { getCurrentUser } from "@robosophy/auth";
80
-
81
- router.beforeEach(async (to) => {
82
- if (!to.meta.requiresAuth) return true;
83
- try {
84
- const user = await getCurrentUser();
85
- return Boolean(user?.rolCYO?.rol);
86
- } catch {
87
- return false; // attachAuthRefresh ya redirigió al login (prod) o avisó por consola (DEV)
88
- }
89
- });
90
- ```
91
-
92
- ```vue
93
- <!-- Botón de salir -->
94
- <script setup>
95
- import { logout } from "@robosophy/auth";
96
- </script>
97
-
98
- <template>
99
- <button @click="logout">Salir</button>
100
- </template>
101
- ```
102
-
103
- ## Uso en React
104
-
105
- ```tsx
106
- // main.tsx
107
- import { configureAuth } from "@robosophy/auth";
108
-
109
- configureAuth({
110
- landingUrl: import.meta.env.VITE_LANDING_URL,
111
- devEmail: import.meta.env.VITE_DEV_EMAIL,
112
- isDev: import.meta.env.DEV,
113
- });
114
- ```
115
-
116
- ```tsx
117
- // useCurrentUser.ts
118
- import { useEffect, useState } from "react";
119
- import { getCurrentUser } from "@robosophy/auth";
120
-
121
- export function useCurrentUser<T>() {
122
- const [user, setUser] = useState<T | null>(null);
123
- useEffect(() => {
124
- getCurrentUser<T>().then(setUser).catch(() => setUser(null));
125
- }, []);
126
- return user;
127
- }
128
- ```
129
-
130
- ```tsx
131
- import { logout } from "@robosophy/auth";
132
-
133
- <button onClick={() => logout()}>Salir</button>;
134
- ```
135
-
136
- ## Desarrollo local
137
-
138
- - La variable con la URL del Landing (`landingUrl`) apunta al **Landing back** en `http://localhost:8184`, no al Vite del front de Landing: ahí están `/api/usuario/self` y `/dev/token`.
139
- - `VITE_DEV_EMAIL=usuario@basf.com` va en `.env.local`, que no se commitea.
140
- - Con el Landing back en perfil `local`, el front pide el token solo en el primer 401: no hace falta loguearse ni copiar tokens.
141
- - Para entrar con otro usuario se cambia el email y se recarga. `configureAuth` detecta el cambio (lo guarda en `localStorage["auth-dev-email"]`) y borra la sesión del usuario anterior.
1
+ # @robosophy/auth
2
+
3
+ Sesión de los fronts de Mi Cuenta contra el Landing: access token RS256 de 15 minutos en `localStorage["token"]`, refresh opaco en cookie HttpOnly y datos del usuario desde `GET /api/usuario/self`.
4
+
5
+ No depende de ningún framework. Necesita `axios` (peer dependency) y `fetch`.
6
+
7
+ ## Instalación
8
+
9
+ ```sh
10
+ npm install @robosophy/auth
11
+ ```
12
+
13
+ ## Configuración
14
+
15
+ Se llama una sola vez, al arrancar la app:
16
+
17
+ ```js
18
+ import { configureAuth } from "@robosophy/auth";
19
+
20
+ configureAuth({
21
+ landingUrl: import.meta.env.VITE_LANDING_URL, // origin del Landing (front y back comparten origin)
22
+ devEmail: import.meta.env.VITE_DEV_EMAIL, // solo DEV
23
+ isDev: import.meta.env.DEV,
24
+ });
25
+ ```
26
+
27
+ `hostsLogin: true` solo en Landing, que es el que tiene el login: redirige a su propio `/login` (`window.location.origin`) y lo hace también en DEV. Los módulos no lo pasan: van a `{landingUrl}/login` y en DEV no redirigen.
28
+
29
+ ## API
30
+
31
+ | Función | Qué hace |
32
+ |---|---|
33
+ | `configureAuth(opts)` | Guarda `landingUrl`, `devEmail`, `isDev` y `hostsLogin`. |
34
+ | `getToken()` | Devuelve `localStorage["token"]` o `null`. |
35
+ | `attachAuthRefresh(api)` | Agrega el Bearer a cada request y ante un 401 renueva el token y reintenta una vez. |
36
+ | `getCurrentUser()` | `GET {landingUrl}/api/usuario/self`, cacheado en memoria. Si falla, el cache se descarta. |
37
+ | `setSession(token)` | Guarda el token nuevo e invalida el cache del usuario. |
38
+ | `logout()` | `POST {landingUrl}/api/auth/logout`, limpia la sesión y va al login (ver `hostsLogin`). |
39
+ | `clearSession()` | Borra `token`, `user-id`, `originalToken` y `token-landing` de `localStorage`. |
40
+
41
+ ## Cómo se renueva el token
42
+
43
+ Ante un 401 de un request que todavía no se reintentó:
44
+
45
+ 1. Se toma el lock `basf-auth-refresh` con `navigator.locks`, compartido entre pestañas. Si el navegador no lo soporta, se deduplica solo dentro de la pestaña.
46
+ 2. Si `localStorage["token"]` ya cambió, otra pestaña renovó primero y se usa ese token.
47
+ 3. Si no, se pide uno nuevo:
48
+ - En DEV con `devEmail`: `POST {landingUrl}/dev/token` con `{ email }`. El Landing back local lo expone solo en el perfil `local`.
49
+ - Si no: `POST {landingUrl}/api/auth/refresh` con `credentials: "include"`, que rota la cookie de refresh.
50
+ 4. Con token nuevo se reintenta el request original. Si no hay token:
51
+ - En DEV: `console.warn("[auth] sin sesión: definí VITE_DEV_EMAIL en .env.local")` y el error sigue. No redirige (salvo con `hostsLogin`).
52
+ - En prod, o con `hostsLogin`: `clearSession()` y redirect al login, salvo que ya esté en esa página.
53
+
54
+ Los 401 de `/api/auth/login`, `/api/auth/verify-2fa`, `/api/auth/resend-2fa`, `/api/auth/refresh`, `/api/auth/logout` y `/dev/token` no disparan el refresh: ahí el 401 es la respuesta (credenciales, código 2FA, refresh vencido). El resto de `/api/auth/*`, como `request-2fa-challenge`, sí renueva y reintenta.
55
+
56
+ ## Uso en Vue
57
+
58
+ ```js
59
+ // main.js
60
+ import { configureAuth } from "@robosophy/auth";
61
+
62
+ configureAuth({
63
+ landingUrl: import.meta.env.VITE_LANDING_URL,
64
+ devEmail: import.meta.env.VITE_DEV_EMAIL,
65
+ isDev: import.meta.env.DEV,
66
+ });
67
+ ```
68
+
69
+ ```js
70
+ // axiosClient.js
71
+ import axios from "axios";
72
+ import { attachAuthRefresh } from "@robosophy/auth";
73
+
74
+ const api = axios.create({ baseURL: import.meta.env.VITE_URL });
75
+ attachAuthRefresh(api);
76
+ export default api;
77
+ ```
78
+
79
+ ```js
80
+ // router/index.js
81
+ import { getCurrentUser } from "@robosophy/auth";
82
+
83
+ router.beforeEach(async (to) => {
84
+ if (!to.meta.requiresAuth) return true;
85
+ try {
86
+ const user = await getCurrentUser();
87
+ return Boolean(user?.rolCYO?.rol);
88
+ } catch {
89
+ return false; // attachAuthRefresh ya redirigió al login (prod) o avisó por consola (DEV)
90
+ }
91
+ });
92
+ ```
93
+
94
+ ```vue
95
+ <!-- Botón de salir -->
96
+ <script setup>
97
+ import { logout } from "@robosophy/auth";
98
+ </script>
99
+
100
+ <template>
101
+ <button @click="logout">Salir</button>
102
+ </template>
103
+ ```
104
+
105
+ ## Uso en React
106
+
107
+ ```tsx
108
+ // main.tsx
109
+ import { configureAuth } from "@robosophy/auth";
110
+
111
+ configureAuth({
112
+ landingUrl: import.meta.env.VITE_LANDING_URL,
113
+ devEmail: import.meta.env.VITE_DEV_EMAIL,
114
+ isDev: import.meta.env.DEV,
115
+ });
116
+ ```
117
+
118
+ ```tsx
119
+ // useCurrentUser.ts
120
+ import { useEffect, useState } from "react";
121
+ import { getCurrentUser } from "@robosophy/auth";
122
+
123
+ export function useCurrentUser<T>() {
124
+ const [user, setUser] = useState<T | null>(null);
125
+ useEffect(() => {
126
+ getCurrentUser<T>().then(setUser).catch(() => setUser(null));
127
+ }, []);
128
+ return user;
129
+ }
130
+ ```
131
+
132
+ ```tsx
133
+ import { logout } from "@robosophy/auth";
134
+
135
+ <button onClick={() => logout()}>Salir</button>;
136
+ ```
137
+
138
+ ## Desarrollo local
139
+
140
+ - La variable con la URL del Landing (`landingUrl`) apunta al **Landing back** en `http://localhost:8184`, no al Vite del front de Landing: ahí están `/api/usuario/self` y `/dev/token`.
141
+ - `VITE_DEV_EMAIL=usuario@basf.com` va en `.env.local`, que no se commitea.
142
+ - Con el Landing back en perfil `local`, el front pide el token solo en el primer 401: no hace falta loguearse ni copiar tokens.
143
+ - Para entrar con otro usuario se cambia el email y se recarga. `configureAuth` detecta el cambio (lo guarda en `localStorage["auth-dev-email"]`) y borra la sesión del usuario anterior.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,11 @@ interface AuthOptions {
5
5
  devEmail?: string;
6
6
  /** import.meta.env.DEV */
7
7
  isDev?: boolean;
8
+ /**
9
+ * Solo Landing, que es el que tiene el login: redirige a su propio /login (window.location.origin,
10
+ * no landingUrl, que en local es el back) y lo hace también en DEV.
11
+ */
12
+ hostsLogin?: boolean;
8
13
  }
9
14
  /**
10
15
  * Lo único que se usa de la instancia. Estructural para no atar los tipos a la versión de axios
package/dist/index.js CHANGED
@@ -24,7 +24,8 @@ function configureAuth(opts) {
24
24
  options = {
25
25
  landingUrl: opts.landingUrl.replace(/\/+$/, ""),
26
26
  devEmail: opts.devEmail ?? "",
27
- isDev: opts.isDev ?? false
27
+ isDev: opts.isDev ?? false,
28
+ hostsLogin: opts.hostsLogin ?? false
28
29
  };
29
30
  landingApi = null;
30
31
  currentUser = null;
@@ -91,7 +92,7 @@ async function logout() {
91
92
  } catch {
92
93
  }
93
94
  clearSession();
94
- window.location.href = `${landingUrl}/login`;
95
+ window.location.href = loginUrl();
95
96
  }
96
97
  function requireOptions() {
97
98
  if (!options) {
@@ -99,6 +100,10 @@ function requireOptions() {
99
100
  }
100
101
  return options;
101
102
  }
103
+ function loginUrl() {
104
+ const { landingUrl, hostsLogin } = requireOptions();
105
+ return `${hostsLogin ? window.location.origin : landingUrl}/login`;
106
+ }
102
107
  function getLandingApi() {
103
108
  if (!landingApi) {
104
109
  landingApi = axios.create({ baseURL: requireOptions().landingUrl });
@@ -160,15 +165,15 @@ async function requestNewToken() {
160
165
  }
161
166
  }
162
167
  function onSessionLost() {
163
- const { landingUrl, isDev } = requireOptions();
164
- if (isDev) {
168
+ const { isDev, hostsLogin } = requireOptions();
169
+ if (isDev && !hostsLogin) {
165
170
  console.warn("[auth] sin sesi\xF3n: defin\xED VITE_DEV_EMAIL en .env.local");
166
171
  return;
167
172
  }
168
173
  clearSession();
169
- const loginUrl = `${landingUrl}/login`;
170
- if (!window.location.href.startsWith(loginUrl)) {
171
- window.location.href = loginUrl;
174
+ const target = loginUrl();
175
+ if (!window.location.href.startsWith(target)) {
176
+ window.location.href = target;
172
177
  }
173
178
  }
174
179
  export {
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
- {
2
- "name": "@robosophy/auth",
3
- "version": "1.0.0",
4
- "private": false,
5
- "license": "UNLICENSED",
6
- "type": "module",
7
- "main": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "default": "./dist/index.js"
13
- }
14
- },
15
- "files": [
16
- "dist",
17
- "README.md",
18
- "package.json"
19
- ],
20
- "sideEffects": false,
21
- "scripts": {
22
- "prepublishOnly": "npm run build",
23
- "build": "tsup src/index.ts --format esm --dts --clean --external axios",
24
- "test": "vitest run"
25
- },
26
- "peerDependencies": {
27
- "axios": ">=0.19.0"
28
- },
29
- "devDependencies": {
30
- "axios": "^1.7.0",
31
- "tsup": "^8.5.0",
32
- "typescript": "^5.9.3",
33
- "vitest": "^4.0.0"
34
- }
35
- }
1
+ {
2
+ "name": "@robosophy/auth",
3
+ "version": "1.1.0",
4
+ "private": false,
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "package.json"
19
+ ],
20
+ "sideEffects": false,
21
+ "scripts": {
22
+ "prepublishOnly": "npm run build",
23
+ "build": "tsup src/index.ts --format esm --dts --clean --external axios",
24
+ "test": "vitest run"
25
+ },
26
+ "peerDependencies": {
27
+ "axios": ">=0.19.0"
28
+ },
29
+ "devDependencies": {
30
+ "axios": "^1.7.0",
31
+ "tsup": "^8.5.0",
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^4.0.0"
34
+ }
35
+ }