@zauru-sdk/utils 6.22.1 → 6.24.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.
@@ -1,6 +1,7 @@
1
1
  import { redirect } from "@remix-run/node";
2
+ import { isOauthUserinfoUnauthorizedError } from "@zauru-sdk/common";
2
3
  import { config } from "@zauru-sdk/config";
3
- import { commitSession, destroySession, getRefreshSession, getSession, loginWebApp, } from "@zauru-sdk/services";
4
+ import { commitSession, destroySession, getHeaders, getRefreshSession, getSession, loginWebApp, } from "@zauru-sdk/services";
4
5
  const loginErrorPayload = (description, title) => ({
5
6
  error: true,
6
7
  type: "error",
@@ -9,11 +10,39 @@ const loginErrorPayload = (description, title) => ({
9
10
  description,
10
11
  });
11
12
  const isDenied = (result) => result === false;
13
+ const isResponse = (value) => typeof Response !== "undefined" && value instanceof Response;
14
+ const deniedLoginPayload = () => loginErrorPayload("No tienes permiso para acceder a esta aplicación. Si crees que esto es un error, contacta al administrador.", "Acceso denegado");
15
+ const evaluateLoginAccess = async (extras, session, headers) => {
16
+ try {
17
+ if (isDenied(await extras?.requireAccess?.(session, headers))) {
18
+ return Response.json(deniedLoginPayload());
19
+ }
20
+ return null;
21
+ }
22
+ catch (error) {
23
+ if (isResponse(error)) {
24
+ throw error;
25
+ }
26
+ const description = error instanceof Error ? error.message : String(error);
27
+ return Response.json(loginErrorPayload(description));
28
+ }
29
+ };
30
+ const OAUTH_REAUTH_COOKIE = "zauru_oauth_reauth";
12
31
  const authorizeUrl = (request) => {
13
32
  const url = new URL(request.url);
14
33
  const hostname = `${url.port ? "http://" : "https://"}${url.hostname}${url.port ? `:${url.port}` : ""}`;
15
34
  return `${config.oauthBaseURL}/dialog/authorize?client_id=${config.oauthClientID}&response_type=code&redirect_uri=${hostname}/login`;
16
35
  };
36
+ const hasOauthReauthCookie = (request) => (request.headers.get("Cookie") ?? "")
37
+ .split(";")
38
+ .some((part) => part.trim() === `${OAUTH_REAUTH_COOKIE}=1`);
39
+ const oauthReauthSetCookie = () => `${OAUTH_REAUTH_COOKIE}=1; Path=/; Max-Age=120; SameSite=Lax`;
40
+ const redirectOauthReauth = async (session) => {
41
+ const headers = new Headers();
42
+ headers.append("Set-Cookie", await destroySession(session));
43
+ headers.append("Set-Cookie", oauthReauthSetCookie());
44
+ return redirect("/logout?reauth=1", { headers });
45
+ };
17
46
  export function createLoginLoader() {
18
47
  return async () => Response.json({});
19
48
  }
@@ -21,6 +50,10 @@ export function createLoginLoader() {
21
50
  * Remix action for `/login`: OAuth authorize, `loginWebApp`, cookie commit.
22
51
  * `afterLogin` runs before `commitSession`. `requireAccess` can deny after
23
52
  * a successful OAuth exchange or when a session already has `code`.
53
+ *
54
+ * HTTP 401 on `/api/userinfo` (expired/stale `code`) clears the Remix session
55
+ * and redirects to `/logout?reauth=1` so `/login` can request a new code.
56
+ * A 120s `zauru_oauth_reauth` cookie prevents a loop if OAuth keeps failing.
24
57
  */
25
58
  export function createLoginAction(extras) {
26
59
  return async ({ request }) => {
@@ -41,17 +74,32 @@ export function createLoginAction(extras) {
41
74
  return redirect(authorizeUrl(request));
42
75
  }
43
76
  if (session.has("code")) {
44
- if (isDenied(await extras?.requireAccess?.(session))) {
77
+ const existingHeaders = await getHeaders(null, session);
78
+ try {
79
+ if (isDenied(await extras?.requireAccess?.(session, existingHeaders))) {
80
+ return redirect("/");
81
+ }
82
+ }
83
+ catch (error) {
84
+ if (isResponse(error)) {
85
+ throw error;
86
+ }
45
87
  return redirect("/");
46
88
  }
47
89
  return redirect("/home");
48
90
  }
49
91
  const loginResponse = await loginWebApp(session, codeValue, cookie);
50
92
  if (loginResponse.error || !loginResponse.data) {
51
- return Response.json(loginErrorPayload(loginResponse.userMsg?.toString() ?? ""));
93
+ const userMsg = loginResponse.userMsg?.toString() ?? "";
94
+ if (isOauthUserinfoUnauthorizedError(userMsg) &&
95
+ !hasOauthReauthCookie(request)) {
96
+ return redirectOauthReauth(session);
97
+ }
98
+ return Response.json(loginErrorPayload(userMsg));
52
99
  }
53
- if (isDenied(await extras?.requireAccess?.(session))) {
54
- return Response.json(loginErrorPayload("No tienes permiso para acceder a esta aplicación. Si crees que esto es un error, contacta al administrador.", "Acceso denegado"));
100
+ const denied = await evaluateLoginAccess(extras, session, loginResponse.data.headers);
101
+ if (denied) {
102
+ return denied;
55
103
  }
56
104
  if (extras?.afterLogin) {
57
105
  await extras.afterLogin({
@@ -78,14 +126,27 @@ export function createSessionGuardLoader(extras) {
78
126
  if (!session.has("username")) {
79
127
  return redirect("/");
80
128
  }
81
- if (isDenied(await extras?.requireAccess?.(session))) {
129
+ const headers = await getHeaders(null, session);
130
+ if (isDenied(await extras?.requireAccess?.(session, headers))) {
82
131
  return redirect("/");
83
132
  }
84
133
  return Response.json({});
85
134
  };
86
135
  }
87
136
  export function createLogoutLoader(extras) {
88
- return createSessionGuardLoader(extras);
137
+ const guard = createSessionGuardLoader(extras);
138
+ return async (args) => {
139
+ const url = new URL(args.request.url);
140
+ if (url.searchParams.get("reauth") === "1") {
141
+ const session = await getSession(args.request.headers.get("Cookie"));
142
+ return redirect("/login", {
143
+ headers: {
144
+ "Set-Cookie": await destroySession(session),
145
+ },
146
+ });
147
+ }
148
+ return guard(args);
149
+ };
89
150
  }
90
151
  export function createLogoutAction() {
91
152
  return async ({ request }) => {
@@ -104,7 +165,8 @@ export function createIndexLoader(extras) {
104
165
  return async ({ request }) => {
105
166
  const session = await getSession(request.headers.get("Cookie"));
106
167
  if (session.has("username")) {
107
- if (isDenied(await extras?.requireAccess?.(session))) {
168
+ const headers = await getHeaders(null, session);
169
+ if (isDenied(await extras?.requireAccess?.(session, headers))) {
108
170
  return Response.json({});
109
171
  }
110
172
  return redirect("/home");
@@ -121,7 +183,8 @@ export function createReloadCatalogsLoader(extras) {
121
183
  if (!session.has("username")) {
122
184
  return redirect("/");
123
185
  }
124
- if (isDenied(await extras?.requireAccess?.(session))) {
186
+ const headers = await getHeaders(null, session);
187
+ if (isDenied(await extras?.requireAccess?.(session, headers))) {
125
188
  return redirect("/");
126
189
  }
127
190
  try {
@@ -1,6 +1,6 @@
1
1
  import { redirect } from "@remix-run/node";
2
2
  import { handlePossibleAxiosErrors } from "@zauru-sdk/common";
3
- import { createWebAppTableRegister, getVariablesByName, getWebAppRow, getWebAppTableRegisters, updateWebAppTableRegister, } from "@zauru-sdk/services";
3
+ import { createWebAppTableRegister, getHeaders, getVariablesByName, getWebAppRow, getWebAppTableRegisters, updateWebAppTableRegister, } from "@zauru-sdk/services";
4
4
  import { hasPermission as hasPermissionInContext, isActiveWebappRow, isSuperAdminEmail, } from "./access.js";
5
5
  import { DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES, isValidPermissionKey, normalizePermissionKeys, } from "./catalog.js";
6
6
  const deletedAt = () => new Date().toISOString();
@@ -11,11 +11,14 @@ const toNumber = (value) => {
11
11
  export function createWebappRbac(config) {
12
12
  const suffixes = config.superAdminEmailSuffixes ?? DEFAULT_SUPER_ADMIN_EMAIL_SUFFIXES;
13
13
  const validKeys = new Set(config.allPermissionKeys);
14
+ const resolveHeaders = async (headers, session) => {
15
+ if (headers?.["X-User-Token"] || headers?.["x-user-token"]) {
16
+ return headers;
17
+ }
18
+ return getHeaders(null, session);
19
+ };
14
20
  const getTableIds = async (headers, session) => {
15
- const vars = await getVariablesByName(headers, session, [
16
- config.rolesTableVar,
17
- config.employeeRolesTableVar,
18
- ]);
21
+ const vars = await getVariablesByName(await resolveHeaders(headers, session), session, [config.rolesTableVar, config.employeeRolesTableVar]);
19
22
  return {
20
23
  rolesTableId: vars[config.rolesTableVar],
21
24
  employeeRolesTableId: vars[config.employeeRolesTableVar],
@@ -210,6 +213,7 @@ export function createWebappRbac(config) {
210
213
  const context = await resolveAccess(headers, session);
211
214
  return context != null;
212
215
  };
216
+ const hasAppAccessFromSession = (session) => hasAppAccess(undefined, session);
213
217
  const requireAppAccess = async (headers, session) => {
214
218
  if (!session.has("username")) {
215
219
  throw redirect("/");
@@ -220,6 +224,7 @@ export function createWebappRbac(config) {
220
224
  }
221
225
  return context;
222
226
  };
227
+ const requireAppAccessFromSession = (session) => requireAppAccess(undefined, session);
223
228
  const requirePermission = async (headers, session, key) => {
224
229
  const context = await requireAppAccess(headers, session);
225
230
  if (!hasPermissionInContext(context, key)) {
@@ -240,7 +245,9 @@ export function createWebappRbac(config) {
240
245
  assignEmployeeRole,
241
246
  resolveAccess,
242
247
  hasAppAccess,
248
+ hasAppAccessFromSession,
243
249
  requireAppAccess,
250
+ requireAppAccessFromSession,
244
251
  requirePermission,
245
252
  hasPermission: hasPermissionInContext,
246
253
  isSuperAdminEmail: (email) => isSuperAdminEmail(email, suffixes),
@@ -6,8 +6,12 @@ export type AuthAccessExtras = {
6
6
  /**
7
7
  * Return `false` to deny. Throw `redirect(...)` to send the user elsewhere.
8
8
  * Any other return (including `void`) allows access.
9
+ *
10
+ * `headers` is present after a successful `loginWebApp`; otherwise it is
11
+ * built from the session (`X-User-Email` / `X-User-Token`). Prefer
12
+ * `rbac.hasAppAccessFromSession` so callers do not pass `{}`.
9
13
  */
10
- requireAccess?: (session: AuthSession) => boolean | void | Promise<boolean | void>;
14
+ requireAccess?: (session: AuthSession, headers?: Record<string, string>) => boolean | void | Promise<boolean | void>;
11
15
  };
12
16
  export type LoginActionExtras = AuthAccessExtras & {
13
17
  afterLogin?: (ctx: {
@@ -22,6 +26,10 @@ export declare function createLoginLoader(): LoaderFunction;
22
26
  * Remix action for `/login`: OAuth authorize, `loginWebApp`, cookie commit.
23
27
  * `afterLogin` runs before `commitSession`. `requireAccess` can deny after
24
28
  * a successful OAuth exchange or when a session already has `code`.
29
+ *
30
+ * HTTP 401 on `/api/userinfo` (expired/stale `code`) clears the Remix session
31
+ * and redirects to `/logout?reauth=1` so `/login` can request a new code.
32
+ * A 120s `zauru_oauth_reauth` cookie prevents a loop if OAuth keeps failing.
25
33
  */
26
34
  export declare function createLoginAction(extras?: LoginActionExtras): ActionFunction;
27
35
  /**
@@ -29,7 +29,9 @@ export declare function createWebappRbac(config: WebappRbacConfig): {
29
29
  assignEmployeeRole: (headers: any, session: Session, body: AssignEmployeeRoleInput) => Promise<AxiosUtilsResponse<WebAppTableUpdateResponse>>;
30
30
  resolveAccess: (headers: any, session: Session) => Promise<WebappAccessContext | null>;
31
31
  hasAppAccess: (headers: any, session: Session) => Promise<boolean>;
32
+ hasAppAccessFromSession: (session: Session) => Promise<boolean>;
32
33
  requireAppAccess: (headers: any, session: Session) => Promise<WebappAccessContext>;
34
+ requireAppAccessFromSession: (session: Session) => Promise<WebappAccessContext>;
33
35
  requirePermission: (headers: any, session: Session, key: string) => Promise<WebappAccessContext>;
34
36
  hasPermission: typeof hasPermissionInContext;
35
37
  isSuperAdminEmail: (email: string | null | undefined) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/utils",
3
- "version": "6.22.1",
3
+ "version": "6.24.0",
4
4
  "description": "Utilidades, parseo de request y demás para desarrollo de webapps de Zauru",
5
5
  "main": "./dist/esm/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -71,12 +71,12 @@
71
71
  "dependencies": {
72
72
  "@aws-sdk/client-cloudwatch-logs": "^3.1123.0",
73
73
  "@remix-run/node": "^2.8.1",
74
- "@zauru-sdk/common": "^6.8.6",
74
+ "@zauru-sdk/common": "^6.9.0",
75
75
  "@zauru-sdk/config": "^6.0.0",
76
76
  "@zauru-sdk/graphql": "^6.8.0",
77
- "@zauru-sdk/services": "^6.11.9",
77
+ "@zauru-sdk/services": "^6.11.10",
78
78
  "@zauru-sdk/types": "^6.24.0",
79
79
  "moment": "^2.30.1"
80
80
  },
81
- "gitHead": "d1667a29d9fabc9980d171fed30796c232de358d"
81
+ "gitHead": "a46ebe234551d76d6a0ea9095be6e55540006ab3"
82
82
  }